[CalendarServer-changes] [4857] CalendarServer/branches/users/cdaboo/deployment-partition-4855
source_changes at macosforge.org
source_changes at macosforge.org
Fri Dec 11 10:41:24 PST 2009
Revision: 4857
http://trac.macosforge.org/projects/calendarserver/changeset/4857
Author: cdaboo at apple.com
Date: 2009-12-11 10:41:21 -0800 (Fri, 11 Dec 2009)
Log Message:
-----------
Merge from deployment "trunk".
Modified Paths:
--------------
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts-test.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.dtd
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd-test.plist
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd.plist
CalendarServer/branches/users/cdaboo/deployment-partition-4855/memcacheclient.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/run
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cache.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cluster.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/config.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/customxml.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/appleopendirectory.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxy.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/directory.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/idirectory.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/principal.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sudo.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/accounts.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_aggregate.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_calendar.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_digest.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_guidchange.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectory.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_principal.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipaldb.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipalmembers.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_util.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_xmlfile.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/util.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaccountsparser.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlfile.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/extensions.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/ical.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/index.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/log.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcachepool.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacheprops.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacher.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/method/report_common.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/notify.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/root.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/schedule.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/static.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/tap.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_cache.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_config.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_log.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcache.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcachepool.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcacher.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_resource.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_root.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_static.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_tap.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/util.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/util.py
Added Paths:
-----------
CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_load_augmentdb
CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_manage_augments
CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/calendarserver_manage_principals
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments-test.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments.dtd
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions-test.plist
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions.plist
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies-test.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies.dtd
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver-test.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver.dtd
CalendarServer/branches/users/cdaboo/deployment-partition-4855/lib-patches/Twisted/twisted.web2.client.http.patch
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/database.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/augment.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxyloader.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test-default.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/proxies.xml
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_augment.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaugmentsparser.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/partitions.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_database.py
Removed Paths:
-------------
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/apache.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sqldb.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/basic
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/digest
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/groups
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_apache.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryrecords.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryschema.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_sqldb.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/itip.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py
CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_load_augmentdb (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/bin/caldav_load_augmentdb)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_load_augmentdb (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_load_augmentdb 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2006-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+ if "PYTHONPATH" in globals():
+ sys.path.insert(0, PYTHONPATH)
+ else:
+ from os.path import dirname, abspath, join
+ from subprocess import Popen, PIPE
+
+ home = dirname(dirname(abspath(__file__)))
+ run = join(home, "run")
+
+ child = Popen((run, "-p"), stdout=PIPE)
+ path, stderr = child.communicate()
+
+ if child.wait() == 0:
+ sys.path[0:0] = path.split(":")
+
+ from calendarserver.tools.loadaugmentdb import main
+ main()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_manage_augments (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/bin/caldav_manage_augments)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_manage_augments (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/caldav_manage_augments 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 2006-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+ if "PYTHONPATH" in globals():
+ sys.path.insert(0, PYTHONPATH)
+ else:
+ from os.path import dirname, abspath, join
+ from subprocess import Popen, PIPE
+
+ home = dirname(dirname(abspath(__file__)))
+ run = join(home, "run")
+
+ child = Popen((run, "-p"), stdout=PIPE)
+ path, stderr = child.communicate()
+
+ if child.wait() == 0:
+ sys.path[0:0] = path.split(":")
+
+ from calendarserver.tools.manageaugments import main
+ main()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/calendarserver_manage_principals (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/bin/calendarserver_manage_principals)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/calendarserver_manage_principals (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/bin/calendarserver_manage_principals 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,41 @@
+#!/usr/bin/env python
+
+##
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import with_statement
+import sys
+
+#PYTHONPATH
+
+if __name__ == "__main__":
+ if "PYTHONPATH" in globals():
+ sys.path.insert(0, PYTHONPATH)
+ else:
+ from os.path import dirname, abspath, join
+ from subprocess import Popen, PIPE
+
+ home = dirname(dirname(abspath(__file__)))
+ run = join(home, "run")
+
+ child = Popen((run, "-p"), stdout=PIPE)
+ path, stderr = child.communicate()
+
+ if child.wait() == 0:
+ sys.path[0:0] = path.split(":")
+
+ from calendarserver.tools.principals import main
+ main()
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/loadaugmentdb.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,185 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from calendarserver.tools.util import loadConfig, getDirectory,\
- autoDisableMemcached
-from getopt import getopt, GetoptError
-from grp import getgrnam
-from pwd import getpwnam
-from sys import stdout, stderr
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.log import addObserver, removeObserver
-from twisted.python.util import switchUID
-from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import augment
-from twistedcaldav.directory.augment import AugmentXMLDB
-from twistedcaldav.log import setLogLevelForNamespace
-import os
-import sys
-
-class UsageError (StandardError):
- pass
-
-class StandardIOObserver (object):
- """
- Log observer that writes to standard I/O.
- """
- def emit(self, eventDict):
- text = None
-
- if eventDict["isError"]:
- output = stderr
- if "failure" in eventDict:
- text = eventDict["failure"].getTraceback()
- else:
- output = stdout
-
- if not text:
- text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
-
- output.write(text)
- output.flush()
-
- def start(self):
- addObserver(self.emit)
-
- def stop(self):
- removeObserver(self.emit)
-
-def usage(e=None):
- if e:
- print e
- print ""
-
- name = os.path.basename(sys.argv[0])
- print "usage: %s [options]" % (name,)
- print ""
- print "Populate an sqlite or PostgreSQL augments database with values"
- print "from an XML augments file."
- print ""
- print "options:"
- print " -h --help: print this help and exit"
- print " -f --config: Specify caldavd.plist configuration path"
- print " -x --xmlfile: Specify xml augments file path"
- print " -r --remove: Remove all entries from the database"
-
- if e:
- sys.exit(64)
- else:
- sys.exit(0)
-
-def main():
- try:
- (optargs, args) = getopt(
- sys.argv[1:], "hf:rx:", [
- "config=",
- "remove",
- "xmlfile=",
- "help",
- ],
- )
- except GetoptError, e:
- usage(e)
-
- configFileName = None
- xmlFileName = None
- remove = False
-
- for opt, arg in optargs:
- if opt in ("-h", "--help"):
- usage()
-
- elif opt in ("-f", "--config"):
- configFileName = arg
-
- elif opt in ("-r", "--remove"):
- remove = True
- if raw_input("Do you really want to remove all records from the database? [y/n] ") != "y":
- sys.exit(0)
-
- elif opt in ("-x", "--xmlfile"):
- xmlFileName = arg
-
- if args:
- usage("Too many arguments: %s" % (" ".join(args),))
-
- observer = StandardIOObserver()
- observer.start()
-
- #
- # Get configuration
- #
- try:
- loadConfig(configFileName)
- setLogLevelForNamespace(None, "warn")
-
- # Shed privileges
- if config.UserName and config.GroupName and os.getuid() == 0:
- uid = getpwnam(config.UserName).pw_uid
- gid = getgrnam(config.GroupName).gr_gid
- switchUID(uid, uid, gid)
-
- os.umask(config.umask)
-
- config.directory = getDirectory()
- autoDisableMemcached(config)
- except ConfigurationError, e:
- usage("Unable to start: %s" % (e,))
-
- try:
- dbxml = AugmentXMLDB((xmlFileName,)) if not remove else None
- except IOError, e:
- usage("Could not read XML augment file: %s" % (e,))
-
- #
- # Start the reactor
- #
- reactor.callLater(0, run, dbxml)
- reactor.run()
-
- at inlineCallbacks
-def run(dbxml):
-
- try:
- guids = set((yield augment.AugmentService.getAllGUIDs()))
- added = 0
- updated = 0
- removed = 0
- if dbxml:
- for record in dbxml.db.values():
- yield augment.AugmentService.addAugmentRecord(record, record.guid in guids)
- if record.guid in guids:
- updated += 1
- else:
- added += 1
- for guid in guids.difference(dbxml.db.keys()):
- yield augment.AugmentService.removeAugmentRecord(guid)
- removed += 1
- else:
- yield augment.AugmentService.clean()
- removed = len(guids)
-
- print "Changes:"
- print " Added: %d" % (added,)
- print " Changed: %d" % (updated,)
- print " Removed: %d" % (removed,)
- finally:
- #
- # Stop the reactor
- #
- reactor.stop()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/loadaugmentdb.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/loadaugmentdb.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,185 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from calendarserver.tools.util import loadConfig, getDirectory,\
+ autoDisableMemcached
+from getopt import getopt, GetoptError
+from grp import getgrnam
+from pwd import getpwnam
+from sys import stdout, stderr
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.python.log import addObserver, removeObserver
+from twisted.python.util import switchUID
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory import augment
+from twistedcaldav.directory.augment import AugmentXMLDB
+from twistedcaldav.log import setLogLevelForNamespace
+import os
+import sys
+
+class UsageError (StandardError):
+ pass
+
+class StandardIOObserver (object):
+ """
+ Log observer that writes to standard I/O.
+ """
+ def emit(self, eventDict):
+ text = None
+
+ if eventDict["isError"]:
+ output = stderr
+ if "failure" in eventDict:
+ text = eventDict["failure"].getTraceback()
+ else:
+ output = stdout
+
+ if not text:
+ text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
+
+ output.write(text)
+ output.flush()
+
+ def start(self):
+ addObserver(self.emit)
+
+ def stop(self):
+ removeObserver(self.emit)
+
+def usage(e=None):
+ if e:
+ print e
+ print ""
+
+ name = os.path.basename(sys.argv[0])
+ print "usage: %s [options]" % (name,)
+ print ""
+ print "Populate an sqlite or PostgreSQL augments database with values"
+ print "from an XML augments file."
+ print ""
+ print "options:"
+ print " -h --help: print this help and exit"
+ print " -f --config: Specify caldavd.plist configuration path"
+ print " -x --xmlfile: Specify xml augments file path"
+ print " -r --remove: Remove all entries from the database"
+
+ if e:
+ sys.exit(64)
+ else:
+ sys.exit(0)
+
+def main():
+ try:
+ (optargs, args) = getopt(
+ sys.argv[1:], "hf:rx:", [
+ "config=",
+ "remove",
+ "xmlfile=",
+ "help",
+ ],
+ )
+ except GetoptError, e:
+ usage(e)
+
+ configFileName = None
+ xmlFileName = None
+ remove = False
+
+ for opt, arg in optargs:
+ if opt in ("-h", "--help"):
+ usage()
+
+ elif opt in ("-f", "--config"):
+ configFileName = arg
+
+ elif opt in ("-r", "--remove"):
+ remove = True
+ if raw_input("Do you really want to remove all records from the database? [y/n] ") != "y":
+ sys.exit(0)
+
+ elif opt in ("-x", "--xmlfile"):
+ xmlFileName = arg
+
+ if args:
+ usage("Too many arguments: %s" % (" ".join(args),))
+
+ observer = StandardIOObserver()
+ observer.start()
+
+ #
+ # Get configuration
+ #
+ try:
+ loadConfig(configFileName)
+ setLogLevelForNamespace(None, "warn")
+
+ # Shed privileges
+ if config.UserName and config.GroupName and os.getuid() == 0:
+ uid = getpwnam(config.UserName).pw_uid
+ gid = getgrnam(config.GroupName).gr_gid
+ switchUID(uid, uid, gid)
+
+ os.umask(config.umask)
+
+ config.directory = getDirectory()
+ autoDisableMemcached(config)
+ except ConfigurationError, e:
+ usage("Unable to start: %s" % (e,))
+
+ try:
+ dbxml = AugmentXMLDB((xmlFileName,)) if not remove else None
+ except IOError, e:
+ usage("Could not read XML augment file: %s" % (e,))
+
+ #
+ # Start the reactor
+ #
+ reactor.callLater(0, run, dbxml)
+ reactor.run()
+
+ at inlineCallbacks
+def run(dbxml):
+
+ try:
+ guids = set((yield augment.AugmentService.getAllGUIDs()))
+ added = 0
+ updated = 0
+ removed = 0
+ if dbxml:
+ for record in dbxml.db.values():
+ yield augment.AugmentService.addAugmentRecord(record, record.guid in guids)
+ if record.guid in guids:
+ updated += 1
+ else:
+ added += 1
+ for guid in guids.difference(dbxml.db.keys()):
+ yield augment.AugmentService.removeAugmentRecord(guid)
+ removed += 1
+ else:
+ yield augment.AugmentService.clean()
+ removed = len(guids)
+
+ print "Changes:"
+ print " Added: %d" % (added,)
+ print " Changed: %d" % (updated,)
+ print " Removed: %d" % (removed,)
+ finally:
+ #
+ # Stop the reactor
+ #
+ reactor.stop()
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/manageaugments.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,200 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from optparse import OptionParser
-from twistedcaldav.directory import xmlaugmentsparser
-from xml.etree.ElementTree import ElementTree, tostring, SubElement
-from xml.parsers.expat import ExpatError
-import sys
-
-def error(s):
- print s
- sys.exit(1)
-
-def readXML(xmlfile):
-
- # Read in XML
- try:
- tree = ElementTree(file=xmlfile)
- except ExpatError, e:
- error("Unable to parse file '%s' because: %s" % (xmlfile, e,))
-
- # Verify that top-level element is correct
- augments_node = tree.getroot()
- if augments_node.tag != xmlaugmentsparser.ELEMENT_AUGMENTS:
- error("Ignoring file '%s' because it is not a augments file" % (xmlfile,))
-
- return augments_node
-
-def writeXML(xmlfile, root):
-
- data = """<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE augments SYSTEM "augments.dtd">
-
-""" + tostring(root)
-
- with open(xmlfile, "w") as f:
- f.write(data)
-
-def addSubElement(parent, tag, text=None, indent=0):
-
- child = SubElement(parent, tag)
- child.text = text
- child.tail = "\n" + " " * indent
- return child
-
-def changeSubElementText(parent, tag, text):
-
- child = parent.find(tag)
- child.text = text
-
-def doAdd(xmlfile, guid, host, enable_calendar, auto_schedule):
-
- augments_node = readXML(xmlfile)
-
- # Make sure GUID is not already present
- for child in augments_node.getchildren():
-
- if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
- error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
-
- for node in child.getchildren():
-
- if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
- error("Cannot add guid '%s' because it already exists in augment file: '%s'" % (guid, xmlfile,))
-
- # Create new record
- augments_node.getchildren()[-1].tail = "\n "
- record = addSubElement(augments_node, xmlaugmentsparser.ELEMENT_RECORD, "\n ")
- addSubElement(record, xmlaugmentsparser.ELEMENT_GUID, guid, 4)
- addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLE, "true", 4)
- addSubElement(record, xmlaugmentsparser.ELEMENT_HOSTEDAT, host, 4)
- addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if enable_calendar else "false", 4)
- addSubElement(record, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if auto_schedule else "false", 2)
-
- # Modify xmlfile
- writeXML(xmlfile, augments_node)
- print "Added guid '%s' in augment file: '%s'" % (guid, xmlfile,)
-
-def doModify(xmlfile, guid, host, enable_calendar, auto_schedule):
-
- augments_node = readXML(xmlfile)
-
- # Make sure GUID is present
- for child in augments_node.getchildren():
-
- if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
- error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
-
- for node in child.getchildren():
-
- if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
- break
- else:
- continue
- break
- else:
- error("Cannot modify guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
-
- # Modify record
- if host is not None:
- child.find(xmlaugmentsparser.ELEMENT_HOSTEDAT).text = host
- child.find(xmlaugmentsparser.ELEMENT_ENABLECALENDAR).text = "true" if enable_calendar else "false"
- child.find(xmlaugmentsparser.ELEMENT_AUTOSCHEDULE).text = "true" if auto_schedule else "false"
-
- # Modify xmlfile
- writeXML(xmlfile, augments_node)
- print "Modified guid '%s' in augment file: '%s'" % (guid, xmlfile,)
-
-def doRemove(xmlfile, guid):
-
- augments_node = readXML(xmlfile)
-
- # Make sure GUID is present
- for child in augments_node.getchildren():
-
- if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
- error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
-
- for node in child.getchildren():
-
- if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
- break
- else:
- continue
- augments_node.remove(child)
- break
- else:
- error("Cannot remove guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
-
- # Modify xmlfile
- writeXML(xmlfile, augments_node)
- print "Removed guid '%s' from augment file: '%s'" % (guid, xmlfile,)
-
-def doPrint(xmlfile):
-
- # Read in XML
- augments_node = readXML(xmlfile)
-
- print tostring(augments_node)
-
-def main():
-
- usage = "%prog [options] ACTION"
- epilog = """
-ACTION is one of add|modify|remove|print
-
- add: add a user record
- modify: modify a user record
- remove: remove a user record
- print: print all user records
-"""
- description = "Tool to manipulate CalendarServer augments XML file"
- version = "%prog v1.0"
- parser = OptionParser(usage=usage, description=description, version=version)
- parser.epilog = epilog
- parser.format_epilog = lambda _:epilog
-
- parser.add_option("-f", "--file", dest="xmlfilename",
- help="XML augment file to manipulate", metavar="FILE")
- parser.add_option("-g", "--guid", dest="guid",
- help="OD GUID to manipulate", metavar="GUID")
- parser.add_option("-n", "--node", dest="node",
- help="Partition node to assign to GUID", metavar="NODE")
- parser.add_option("-c", "--enable-calendar", action="store_true", dest="enable_calendar",
- default=True, help="Enable calendaring for this GUID: %default")
- parser.add_option("-a", "--auto-schedule", action="store_true", dest="auto_schedule",
- default=False, help="Enable auto-schedule for this GUID: %default")
-
- (options, args) = parser.parse_args()
-
- if len(args) != 1:
- parser.error("incorrect number of arguments")
-
- if args[0] == "add":
- if not options.node:
- parser.error("Partition node must be specified when adding")
- doAdd(options.xmlfilename, options.guid, options.node, options.enable_calendar, options.auto_schedule)
- elif args[0] == "modify":
- doModify(options.xmlfilename, options.guid, options.node, options.enable_calendar, options.auto_schedule)
- elif args[0] == "remove":
- doRemove(options.xmlfilename, options.guid)
- elif args[0] == "print":
- doPrint(options.xmlfilename)
-
-if __name__ == '__main__':
- main()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/manageaugments.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/manageaugments.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,200 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from optparse import OptionParser
+from twistedcaldav.directory import xmlaugmentsparser
+from xml.etree.ElementTree import ElementTree, tostring, SubElement
+from xml.parsers.expat import ExpatError
+import sys
+
+def error(s):
+ print s
+ sys.exit(1)
+
+def readXML(xmlfile):
+
+ # Read in XML
+ try:
+ tree = ElementTree(file=xmlfile)
+ except ExpatError, e:
+ error("Unable to parse file '%s' because: %s" % (xmlfile, e,))
+
+ # Verify that top-level element is correct
+ augments_node = tree.getroot()
+ if augments_node.tag != xmlaugmentsparser.ELEMENT_AUGMENTS:
+ error("Ignoring file '%s' because it is not a augments file" % (xmlfile,))
+
+ return augments_node
+
+def writeXML(xmlfile, root):
+
+ data = """<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE augments SYSTEM "augments.dtd">
+
+""" + tostring(root)
+
+ with open(xmlfile, "w") as f:
+ f.write(data)
+
+def addSubElement(parent, tag, text=None, indent=0):
+
+ child = SubElement(parent, tag)
+ child.text = text
+ child.tail = "\n" + " " * indent
+ return child
+
+def changeSubElementText(parent, tag, text):
+
+ child = parent.find(tag)
+ child.text = text
+
+def doAdd(xmlfile, guid, host, enable_calendar, auto_schedule):
+
+ augments_node = readXML(xmlfile)
+
+ # Make sure GUID is not already present
+ for child in augments_node.getchildren():
+
+ if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+ error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+ for node in child.getchildren():
+
+ if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+ error("Cannot add guid '%s' because it already exists in augment file: '%s'" % (guid, xmlfile,))
+
+ # Create new record
+ augments_node.getchildren()[-1].tail = "\n "
+ record = addSubElement(augments_node, xmlaugmentsparser.ELEMENT_RECORD, "\n ")
+ addSubElement(record, xmlaugmentsparser.ELEMENT_GUID, guid, 4)
+ addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLE, "true", 4)
+ addSubElement(record, xmlaugmentsparser.ELEMENT_HOSTEDAT, host, 4)
+ addSubElement(record, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if enable_calendar else "false", 4)
+ addSubElement(record, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if auto_schedule else "false", 2)
+
+ # Modify xmlfile
+ writeXML(xmlfile, augments_node)
+ print "Added guid '%s' in augment file: '%s'" % (guid, xmlfile,)
+
+def doModify(xmlfile, guid, host, enable_calendar, auto_schedule):
+
+ augments_node = readXML(xmlfile)
+
+ # Make sure GUID is present
+ for child in augments_node.getchildren():
+
+ if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+ error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+ for node in child.getchildren():
+
+ if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+ break
+ else:
+ continue
+ break
+ else:
+ error("Cannot modify guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
+
+ # Modify record
+ if host is not None:
+ child.find(xmlaugmentsparser.ELEMENT_HOSTEDAT).text = host
+ child.find(xmlaugmentsparser.ELEMENT_ENABLECALENDAR).text = "true" if enable_calendar else "false"
+ child.find(xmlaugmentsparser.ELEMENT_AUTOSCHEDULE).text = "true" if auto_schedule else "false"
+
+ # Modify xmlfile
+ writeXML(xmlfile, augments_node)
+ print "Modified guid '%s' in augment file: '%s'" % (guid, xmlfile,)
+
+def doRemove(xmlfile, guid):
+
+ augments_node = readXML(xmlfile)
+
+ # Make sure GUID is present
+ for child in augments_node.getchildren():
+
+ if child.tag != xmlaugmentsparser.ELEMENT_RECORD:
+ error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, xmlfile,))
+
+ for node in child.getchildren():
+
+ if node.tag == xmlaugmentsparser.ELEMENT_GUID and node.text == guid:
+ break
+ else:
+ continue
+ augments_node.remove(child)
+ break
+ else:
+ error("Cannot remove guid '%s' because it does not exist in augment file: '%s'" % (guid, xmlfile,))
+
+ # Modify xmlfile
+ writeXML(xmlfile, augments_node)
+ print "Removed guid '%s' from augment file: '%s'" % (guid, xmlfile,)
+
+def doPrint(xmlfile):
+
+ # Read in XML
+ augments_node = readXML(xmlfile)
+
+ print tostring(augments_node)
+
+def main():
+
+ usage = "%prog [options] ACTION"
+ epilog = """
+ACTION is one of add|modify|remove|print
+
+ add: add a user record
+ modify: modify a user record
+ remove: remove a user record
+ print: print all user records
+"""
+ description = "Tool to manipulate CalendarServer augments XML file"
+ version = "%prog v1.0"
+ parser = OptionParser(usage=usage, description=description, version=version)
+ parser.epilog = epilog
+ parser.format_epilog = lambda _:epilog
+
+ parser.add_option("-f", "--file", dest="xmlfilename",
+ help="XML augment file to manipulate", metavar="FILE")
+ parser.add_option("-g", "--guid", dest="guid",
+ help="OD GUID to manipulate", metavar="GUID")
+ parser.add_option("-n", "--node", dest="node",
+ help="Partition node to assign to GUID", metavar="NODE")
+ parser.add_option("-c", "--enable-calendar", action="store_true", dest="enable_calendar",
+ default=True, help="Enable calendaring for this GUID: %default")
+ parser.add_option("-a", "--auto-schedule", action="store_true", dest="auto_schedule",
+ default=False, help="Enable auto-schedule for this GUID: %default")
+
+ (options, args) = parser.parse_args()
+
+ if len(args) != 1:
+ parser.error("incorrect number of arguments")
+
+ if args[0] == "add":
+ if not options.node:
+ parser.error("Partition node must be specified when adding")
+ doAdd(options.xmlfilename, options.guid, options.node, options.enable_calendar, options.auto_schedule)
+ elif args[0] == "modify":
+ doModify(options.xmlfilename, options.guid, options.node, options.enable_calendar, options.auto_schedule)
+ elif args[0] == "remove":
+ doRemove(options.xmlfilename, options.guid)
+ elif args[0] == "print":
+ doPrint(options.xmlfilename)
+
+if __name__ == '__main__':
+ main()
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/managepostgres.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,114 +0,0 @@
-#!/usr/bin/env python
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from optparse import OptionParser
-import os
-import subprocess
-import sys
-import time
-
-def error(s):
- print s
- sys.exit(1)
-
-def cmd(s):
- print s
- subprocess.call(s, shell=True)
-
-def doInit(basedir):
-
- cmd("mkdir %s/data" % (basedir,))
- cmd("%s/bin/initdb -D %s/data" % (basedir, basedir,))
-
- # Have the DB listen on all interfaces
- with open("%s/data/postgresql.conf" % (basedir,)) as f:
- conf = f.read()
- conf = conf.replace("#listen_addresses = 'localhost'", "listen_addresses = '*'\t")
- conf = conf.replace("max_connections = 20 ", "max_connections = 500")
- with open("%s/data/postgresql.conf" % (basedir,), "w") as f:
- f.write(conf)
-
- # Allow current user to auth to the DBs
- with open("%s/data/pg_hba.conf" % (basedir,)) as f:
- conf = f.read()
- conf = conf.replace("127.0.0.1/32", "0.0.0.0/0 ")
- with open("%s/data/pg_hba.conf" % (basedir,), "w") as f:
- f.write(conf)
-
- cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
- time.sleep(5)
- cmd("%s/bin/createdb proxies" % (basedir,))
- cmd("%s/bin/createdb augments" % (basedir,))
- cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
-
-def doStart(basedir):
-
- cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
-
-def doStop(basedir):
-
- cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
-
-def doRun(basedir, verbose):
-
- cmd("%s/bin/postgres %s -D %s/data" % (basedir, "-d 3" if verbose else "", basedir,))
-
-def doClean(basedir):
-
- cmd("rm -rf %s/data" % (basedir, basedir,))
-
-if __name__ == '__main__':
-
- usage = "%prog [options] ACTION"
- epilog = """
-ACTION is one of init|start|stop|run
-
- init: initialize databases
- start: start postgres daemon
- stop: stop postgres daemon
- run: run postgres (non-daemon)
- clean: remove databases
-
-"""
- description = "Tool to manage PostgreSQL"
- version = "%prog v1.0"
- parser = OptionParser(usage=usage, description=description, version=version)
- parser.epilog = epilog
- parser.format_epilog = lambda _:epilog
-
- parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
- default=True, help="Use debug logging for PostgreSQL")
- parser.add_option("-d", "--base-dir", dest="basedir",
- default="%s/../postgresql-8.4.1/_root" % (os.getcwd(),), help="Base directory for PostgreSQL install")
-
- (options, args) = parser.parse_args()
-
- if len(args) != 1:
- parser.error("incorrect number of arguments")
-
- if args[0] == "init":
- doInit(options.basedir)
- elif args[0] == "start":
- doStart(options.basedir)
- elif args[0] == "stop":
- doStop(options.basedir)
- elif args[0] == "run":
- doRun(options.basedir, options.verbose)
- elif args[0] == "clean":
- doClean(options.basedir)
- else:
- parser.error("incorrect argument '%s'" % (args[0],))
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/managepostgres.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/managepostgres.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,114 @@
+#!/usr/bin/env python
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from optparse import OptionParser
+import os
+import subprocess
+import sys
+import time
+
+def error(s):
+ print s
+ sys.exit(1)
+
+def cmd(s):
+ print s
+ subprocess.call(s, shell=True)
+
+def doInit(basedir):
+
+ cmd("mkdir %s/data" % (basedir,))
+ cmd("%s/bin/initdb -D %s/data" % (basedir, basedir,))
+
+ # Have the DB listen on all interfaces
+ with open("%s/data/postgresql.conf" % (basedir,)) as f:
+ conf = f.read()
+ conf = conf.replace("#listen_addresses = 'localhost'", "listen_addresses = '*'\t")
+ conf = conf.replace("max_connections = 20 ", "max_connections = 500")
+ with open("%s/data/postgresql.conf" % (basedir,), "w") as f:
+ f.write(conf)
+
+ # Allow current user to auth to the DBs
+ with open("%s/data/pg_hba.conf" % (basedir,)) as f:
+ conf = f.read()
+ conf = conf.replace("127.0.0.1/32", "0.0.0.0/0 ")
+ with open("%s/data/pg_hba.conf" % (basedir,), "w") as f:
+ f.write(conf)
+
+ cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
+ time.sleep(5)
+ cmd("%s/bin/createdb proxies" % (basedir,))
+ cmd("%s/bin/createdb augments" % (basedir,))
+ cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
+
+def doStart(basedir):
+
+ cmd("%s/bin/pg_ctl -D %s/data -l logfile start" % (basedir, basedir,))
+
+def doStop(basedir):
+
+ cmd("%s/bin/pg_ctl -D %s/data -l logfile stop" % (basedir, basedir,))
+
+def doRun(basedir, verbose):
+
+ cmd("%s/bin/postgres %s -D %s/data" % (basedir, "-d 3" if verbose else "", basedir,))
+
+def doClean(basedir):
+
+ cmd("rm -rf %s/data" % (basedir, basedir,))
+
+if __name__ == '__main__':
+
+ usage = "%prog [options] ACTION"
+ epilog = """
+ACTION is one of init|start|stop|run
+
+ init: initialize databases
+ start: start postgres daemon
+ stop: stop postgres daemon
+ run: run postgres (non-daemon)
+ clean: remove databases
+
+"""
+ description = "Tool to manage PostgreSQL"
+ version = "%prog v1.0"
+ parser = OptionParser(usage=usage, description=description, version=version)
+ parser.epilog = epilog
+ parser.format_epilog = lambda _:epilog
+
+ parser.add_option("-v", "--verbose", action="store_true", dest="verbose",
+ default=True, help="Use debug logging for PostgreSQL")
+ parser.add_option("-d", "--base-dir", dest="basedir",
+ default="%s/../postgresql-8.4.1/_root" % (os.getcwd(),), help="Base directory for PostgreSQL install")
+
+ (options, args) = parser.parse_args()
+
+ if len(args) != 1:
+ parser.error("incorrect number of arguments")
+
+ if args[0] == "init":
+ doInit(options.basedir)
+ elif args[0] == "start":
+ doStart(options.basedir)
+ elif args[0] == "stop":
+ doStop(options.basedir)
+ elif args[0] == "run":
+ doRun(options.basedir, options.verbose)
+ elif args[0] == "clean":
+ doClean(options.basedir)
+ else:
+ parser.error("incorrect argument '%s'" % (args[0],))
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/principals.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,445 +0,0 @@
-#!/usr/bin/env python
-
-##
-# 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import sys
-import os
-from getopt import getopt, GetoptError
-from uuid import UUID
-from pwd import getpwnam
-from grp import getgrnam
-
-from twisted.python.util import switchUID
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks
-from twisted.web2.dav import davxml
-
-from twext.python.log import StandardIOObserver
-from twext.web2.dav.davxml import sname2qname, qname2sname
-
-from twistedcaldav import memcachepool
-from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.log import setLogLevelForNamespace
-from twistedcaldav.notify import installNotificationClient
-from twistedcaldav.root import RootResource
-from twistedcaldav.static import CalendarHomeProvisioningFile
-
-from calendarserver.tools.util import booleanArgument, autoDisableMemcached
-from calendarserver.tools.util import loadConfig, getDirectory
-
-def usage(e=None):
- if e:
- print e
- print ""
-
- name = os.path.basename(sys.argv[0])
- print "usage: %s [options] actions principal [principal ...]" % (name,)
- print ""
- print " Performs the given actions against the giving principals."
- print ""
- print " Principals are identified by one of the following:"
- print " Type and shortname (eg.: users:wsanchez)"
- #print " A principal path (eg.: /principals/users/wsanchez/)"
- print " A GUID (eg.: E415DBA7-40B5-49F5-A7CC-ACC81E4DEC79)"
- print ""
- print "options:"
- print " -h --help: print this help and exit"
- print " -f --config <path>: Specify caldavd.plist configuration path"
- print ""
- print "actions:"
- #print " --search <search-string>: search for matching resources"
- print " -P, --read-property=property: read DAV property (eg.: {DAV:}group-member-set)"
- print " --list-read-proxies: list proxies with read-only access"
- print " --list-write-proxies: list proxies with read-write access"
- print " --list-proxies: list all proxies"
- print " --add-read-proxy=principal: add a read-only proxy"
- print " --add-write-proxy=principal: add a read-write proxy"
- print " --remove-proxy=principal: remove a proxy"
- print " --set-auto-schedule={true|false}: set auto-accept state"
- print " --get-auto-schedule: read auto-schedule state"
-
- if e:
- sys.exit(64)
- else:
- sys.exit(0)
-
-def main():
- #
- # Send logging output to stdout
- #
- observer = StandardIOObserver()
- observer.start()
-
- try:
- (optargs, args) = getopt(
- sys.argv[1:], "hf:P:", [
- "help",
- "config=",
- "read-property=",
- "list-read-proxies",
- "list-write-proxies",
- "list-proxies",
- "add-read-proxy=",
- "add-write-proxy=",
- "remove-proxy=",
- "set-auto-schedule=",
- "get-auto-schedule",
- ],
- )
- except GetoptError, e:
- usage(e)
-
- #
- # Get configuration
- #
- configFileName = None
- actions = []
-
- for opt, arg in optargs:
- if opt in ("-h", "--help"):
- usage()
-
- elif opt in ("-f", "--config"):
- configFileName = arg
-
- elif opt in ("-P", "--read-property"):
- try:
- qname = sname2qname(arg)
- except ValueError, e:
- abort(e)
- actions.append((action_readProperty, qname))
-
- elif opt in ("", "--list-read-proxies"):
- actions.append((action_listProxies, "read"))
-
- elif opt in ("", "--list-write-proxies"):
- actions.append((action_listProxies, "write"))
-
- elif opt in ("-L", "--list-proxies"):
- actions.append((action_listProxies, "read", "write"))
-
- elif opt in ("--add-read-proxy", "--add-write-proxy"):
- if "read" in opt:
- proxyType = "read"
- elif "write" in opt:
- proxyType = "write"
- else:
- raise AssertionError("Unknown proxy type")
-
- try:
- principalForPrincipalID(arg, checkOnly=True)
- except ValueError, e:
- abort(e)
-
- actions.append((action_addProxy, proxyType, arg))
-
- elif opt in ("", "--remove-proxy"):
- try:
- principalForPrincipalID(arg, checkOnly=True)
- except ValueError, e:
- abort(e)
-
- actions.append((action_removeProxy, arg))
-
- elif opt in ("", "--set-auto-schedule"):
- try:
- autoSchedule = booleanArgument(arg)
- except ValueError, e:
- abort(e)
-
- actions.append((action_setAutoSchedule, autoSchedule))
-
- elif opt in ("", "--get-auto-schedule"):
- actions.append((action_getAutoSchedule,))
-
- else:
- raise NotImplementedError(opt)
-
- if not args:
- usage("No principals specified.")
-
- #
- # Get configuration
- #
- try:
- loadConfig(configFileName)
- setLogLevelForNamespace(None, "warn")
-
- # Shed privileges
- if config.UserName and config.GroupName and os.getuid() == 0:
- uid = getpwnam(config.UserName).pw_uid
- gid = getgrnam(config.GroupName).gr_gid
- switchUID(uid, uid, gid)
-
- os.umask(config.umask)
-
- config.directory = getDirectory()
- autoDisableMemcached(config)
- except ConfigurationError, e:
- abort(e)
-
- #
- # Do a quick sanity check that arguments look like principal
- # identifiers.
- #
- for arg in args:
- try:
- principalForPrincipalID(arg, checkOnly=True)
- except ValueError, e:
- abort(e)
-
- #
- # Start the reactor
- #
- reactor.callLater(0, run, args, actions)
- reactor.run()
-
- at inlineCallbacks
-def run(principalIDs, actions):
- try:
- #
- # Connect to memcached, notifications
- #
- memcachepool.installPools(
- config.Memcached.Pools,
- config.Memcached.MaxClients
- )
- if config.Notifications.Enabled:
- installNotificationClient(
- config.Notifications.InternalNotificationHost,
- config.Notifications.InternalNotificationPort,
- )
-
- #
- # Wire up the resource hierarchy
- #
- principalCollection = config.directory.getPrincipalCollection()
- root = RootResource(
- config.DocumentRoot,
- principalCollections=(principalCollection,),
- )
- root.putChild("principals", principalCollection)
- calendarCollection = CalendarHomeProvisioningFile(
- os.path.join(config.DocumentRoot, "calendars"),
- config.directory, "/calendars/",
- )
- root.putChild("calendars", calendarCollection)
-
- #
- # Wrap root resource
- #
- # FIXME: not a fan -wsanchez
- #root = ResourceWrapper(root)
-
- for principalID in principalIDs:
- # Resolve the given principal IDs to principals
- try:
- principal = principalForPrincipalID(principalID)
- except ValueError:
- principal = None
-
- if principal is None:
- sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
- continue
-
- # Performs requested actions
- for action in actions:
- (yield action[0](principal, *action[1:]))
- print ""
-
- finally:
- #
- # Stop the reactor
- #
- reactor.stop()
-
-def principalForPrincipalID(principalID, checkOnly=False, directory=None):
-
- # Allow a directory parameter to be passed in, but default to config.directory
- # But config.directory isn't set right away, so only use it when we're doing more
- # than checking.
- if not checkOnly and not directory:
- directory = config.directory
-
- if principalID.startswith("/"):
- raise ValueError("Can't resolve paths yet")
-
- if checkOnly:
- return None
-
- if principalID.startswith("("):
- try:
- i = principalID.index(")")
-
- if checkOnly:
- return None
-
- recordType = principalID[1:i]
- shortName = principalID[i+1:]
-
- if not recordType or not shortName or "(" in recordType:
- raise ValueError()
-
- return directory.principalCollection.principalForShortName(recordType, shortName)
-
- except ValueError:
- pass
-
- if ":" in principalID:
- if checkOnly:
- return None
-
- recordType, shortName = principalID.split(":", 1)
-
- return directory.principalCollection.principalForShortName(recordType, shortName)
-
- try:
- guid = UUID(principalID)
-
- if checkOnly:
- return None
-
- return directory.principalCollection.principalForUID(guid)
- except ValueError:
- pass
-
- raise ValueError("Invalid principal identifier: %s" % (principalID,))
-
-def proxySubprincipal(principal, proxyType):
- return principal.getChild("calendar-proxy-" + proxyType)
-
- at inlineCallbacks
-def action_readProperty(resource, qname):
- property = (yield resource.readProperty(qname, None))
- print "%r on %s:" % (qname2sname(qname), resource)
- print ""
- print property.toxml()
-
- at inlineCallbacks
-def action_listProxies(principal, *proxyTypes):
- for proxyType in proxyTypes:
- subPrincipal = proxySubprincipal(principal, proxyType)
- if subPrincipal is None:
- print "No %s proxies for %s" % (proxyType, principal)
- continue
-
- membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
- if membersProperty.children:
- print "%s proxies for %s:" % (
- {"read": "Read-only", "write": "Read/write"}[proxyType],
- principal,
- )
- for member in membersProperty.children:
- print " *", member
- else:
- print "No %s proxies for %s" % (proxyType, principal)
-
- at inlineCallbacks
-def action_addProxy(principal, proxyType, *proxyIDs):
- for proxyID in proxyIDs:
- proxyPrincipal = principalForPrincipalID(proxyID)
- (yield action_addProxyPrincipal(principal, proxyType, proxyPrincipal))
-
- at inlineCallbacks
-def action_addProxyPrincipal(principal, proxyType, proxyPrincipal):
- proxyURL = proxyPrincipal.url()
-
- subPrincipal = proxySubprincipal(principal, proxyType)
- if subPrincipal is None:
- sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
- return
-
- membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
- for memberURL in membersProperty.children:
- if str(memberURL) == proxyURL:
- print "%s is already a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
- break
- else:
- memberURLs = list(membersProperty.children)
- memberURLs.append(davxml.HRef(proxyURL))
- membersProperty = davxml.GroupMemberSet(*memberURLs)
- (yield subPrincipal.writeProperty(membersProperty, None))
- print "Added %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
-
- proxyTypes = ["read", "write"]
- proxyTypes.remove(proxyType)
-
- (yield action_removeProxyPrincipal(principal, proxyPrincipal, proxyTypes=proxyTypes))
-
- at inlineCallbacks
-def action_removeProxy(principal, *proxyIDs, **kwargs):
- for proxyID in proxyIDs:
- proxyPrincipal = principalForPrincipalID(proxyID)
- (yield action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs))
-
- at inlineCallbacks
-def action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs):
- proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
- for proxyType in proxyTypes:
- proxyURL = proxyPrincipal.url()
-
- subPrincipal = proxySubprincipal(principal, proxyType)
- if subPrincipal is None:
- sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
- continue
-
- membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-
- memberURLs = [
- m for m in membersProperty.children
- if str(m) != proxyURL
- ]
-
- if len(memberURLs) == len(membersProperty.children):
- # No change
- continue
-
- membersProperty = davxml.GroupMemberSet(*memberURLs)
- (yield subPrincipal.writeProperty(membersProperty, None))
- print "Removed %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
-
- at inlineCallbacks
-def action_setAutoSchedule(principal, autoSchedule):
- if autoSchedule and principal.record.recordType in ("users", "groups"):
- print "Enabling auto-schedule for %s is not allowed." % (principal,)
- else:
- print "Setting auto-schedule to %s for %s" % (
- { True: "true", False: "false" }[autoSchedule],
- principal,
- )
- principal.setAutoSchedule(autoSchedule)
-
- at inlineCallbacks
-def action_getAutoSchedule(principal):
- autoSchedule = principal.getAutoSchedule()
- print "Autoschedule for %s is %s" % (
- principal,
- { True: "true", False: "false" }[autoSchedule],
- )
-
-def abort(msg, status=1):
- sys.stdout.write("%s\n" % (msg,))
- try:
- reactor.stop()
- except RuntimeError:
- pass
- sys.exit(status)
-
-if __name__ == "__main__":
- main()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/principals.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/principals.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,445 @@
+#!/usr/bin/env python
+
+##
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import sys
+import os
+from getopt import getopt, GetoptError
+from uuid import UUID
+from pwd import getpwnam
+from grp import getgrnam
+
+from twisted.python.util import switchUID
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks
+from twisted.web2.dav import davxml
+
+from twext.python.log import StandardIOObserver
+from twext.web2.dav.davxml import sname2qname, qname2sname
+
+from twistedcaldav import memcachepool
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.log import setLogLevelForNamespace
+from twistedcaldav.notify import installNotificationClient
+from twistedcaldav.root import RootResource
+from twistedcaldav.static import CalendarHomeProvisioningFile
+
+from calendarserver.tools.util import booleanArgument, autoDisableMemcached
+from calendarserver.tools.util import loadConfig, getDirectory
+
+def usage(e=None):
+ if e:
+ print e
+ print ""
+
+ name = os.path.basename(sys.argv[0])
+ print "usage: %s [options] actions principal [principal ...]" % (name,)
+ print ""
+ print " Performs the given actions against the giving principals."
+ print ""
+ print " Principals are identified by one of the following:"
+ print " Type and shortname (eg.: users:wsanchez)"
+ #print " A principal path (eg.: /principals/users/wsanchez/)"
+ print " A GUID (eg.: E415DBA7-40B5-49F5-A7CC-ACC81E4DEC79)"
+ print ""
+ print "options:"
+ print " -h --help: print this help and exit"
+ print " -f --config <path>: Specify caldavd.plist configuration path"
+ print ""
+ print "actions:"
+ #print " --search <search-string>: search for matching resources"
+ print " -P, --read-property=property: read DAV property (eg.: {DAV:}group-member-set)"
+ print " --list-read-proxies: list proxies with read-only access"
+ print " --list-write-proxies: list proxies with read-write access"
+ print " --list-proxies: list all proxies"
+ print " --add-read-proxy=principal: add a read-only proxy"
+ print " --add-write-proxy=principal: add a read-write proxy"
+ print " --remove-proxy=principal: remove a proxy"
+ print " --set-auto-schedule={true|false}: set auto-accept state"
+ print " --get-auto-schedule: read auto-schedule state"
+
+ if e:
+ sys.exit(64)
+ else:
+ sys.exit(0)
+
+def main():
+ #
+ # Send logging output to stdout
+ #
+ observer = StandardIOObserver()
+ observer.start()
+
+ try:
+ (optargs, args) = getopt(
+ sys.argv[1:], "hf:P:", [
+ "help",
+ "config=",
+ "read-property=",
+ "list-read-proxies",
+ "list-write-proxies",
+ "list-proxies",
+ "add-read-proxy=",
+ "add-write-proxy=",
+ "remove-proxy=",
+ "set-auto-schedule=",
+ "get-auto-schedule",
+ ],
+ )
+ except GetoptError, e:
+ usage(e)
+
+ #
+ # Get configuration
+ #
+ configFileName = None
+ actions = []
+
+ for opt, arg in optargs:
+ if opt in ("-h", "--help"):
+ usage()
+
+ elif opt in ("-f", "--config"):
+ configFileName = arg
+
+ elif opt in ("-P", "--read-property"):
+ try:
+ qname = sname2qname(arg)
+ except ValueError, e:
+ abort(e)
+ actions.append((action_readProperty, qname))
+
+ elif opt in ("", "--list-read-proxies"):
+ actions.append((action_listProxies, "read"))
+
+ elif opt in ("", "--list-write-proxies"):
+ actions.append((action_listProxies, "write"))
+
+ elif opt in ("-L", "--list-proxies"):
+ actions.append((action_listProxies, "read", "write"))
+
+ elif opt in ("--add-read-proxy", "--add-write-proxy"):
+ if "read" in opt:
+ proxyType = "read"
+ elif "write" in opt:
+ proxyType = "write"
+ else:
+ raise AssertionError("Unknown proxy type")
+
+ try:
+ principalForPrincipalID(arg, checkOnly=True)
+ except ValueError, e:
+ abort(e)
+
+ actions.append((action_addProxy, proxyType, arg))
+
+ elif opt in ("", "--remove-proxy"):
+ try:
+ principalForPrincipalID(arg, checkOnly=True)
+ except ValueError, e:
+ abort(e)
+
+ actions.append((action_removeProxy, arg))
+
+ elif opt in ("", "--set-auto-schedule"):
+ try:
+ autoSchedule = booleanArgument(arg)
+ except ValueError, e:
+ abort(e)
+
+ actions.append((action_setAutoSchedule, autoSchedule))
+
+ elif opt in ("", "--get-auto-schedule"):
+ actions.append((action_getAutoSchedule,))
+
+ else:
+ raise NotImplementedError(opt)
+
+ if not args:
+ usage("No principals specified.")
+
+ #
+ # Get configuration
+ #
+ try:
+ loadConfig(configFileName)
+ setLogLevelForNamespace(None, "warn")
+
+ # Shed privileges
+ if config.UserName and config.GroupName and os.getuid() == 0:
+ uid = getpwnam(config.UserName).pw_uid
+ gid = getgrnam(config.GroupName).gr_gid
+ switchUID(uid, uid, gid)
+
+ os.umask(config.umask)
+
+ config.directory = getDirectory()
+ autoDisableMemcached(config)
+ except ConfigurationError, e:
+ abort(e)
+
+ #
+ # Do a quick sanity check that arguments look like principal
+ # identifiers.
+ #
+ for arg in args:
+ try:
+ principalForPrincipalID(arg, checkOnly=True)
+ except ValueError, e:
+ abort(e)
+
+ #
+ # Start the reactor
+ #
+ reactor.callLater(0, run, args, actions)
+ reactor.run()
+
+ at inlineCallbacks
+def run(principalIDs, actions):
+ try:
+ #
+ # Connect to memcached, notifications
+ #
+ memcachepool.installPools(
+ config.Memcached.Pools,
+ config.Memcached.MaxClients
+ )
+ if config.Notifications.Enabled:
+ installNotificationClient(
+ config.Notifications.InternalNotificationHost,
+ config.Notifications.InternalNotificationPort,
+ )
+
+ #
+ # Wire up the resource hierarchy
+ #
+ principalCollection = config.directory.getPrincipalCollection()
+ root = RootResource(
+ config.DocumentRoot,
+ principalCollections=(principalCollection,),
+ )
+ root.putChild("principals", principalCollection)
+ calendarCollection = CalendarHomeProvisioningFile(
+ os.path.join(config.DocumentRoot, "calendars"),
+ config.directory, "/calendars/",
+ )
+ root.putChild("calendars", calendarCollection)
+
+ #
+ # Wrap root resource
+ #
+ # FIXME: not a fan -wsanchez
+ #root = ResourceWrapper(root)
+
+ for principalID in principalIDs:
+ # Resolve the given principal IDs to principals
+ try:
+ principal = principalForPrincipalID(principalID)
+ except ValueError:
+ principal = None
+
+ if principal is None:
+ sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
+ continue
+
+ # Performs requested actions
+ for action in actions:
+ (yield action[0](principal, *action[1:]))
+ print ""
+
+ finally:
+ #
+ # Stop the reactor
+ #
+ reactor.stop()
+
+def principalForPrincipalID(principalID, checkOnly=False, directory=None):
+
+ # Allow a directory parameter to be passed in, but default to config.directory
+ # But config.directory isn't set right away, so only use it when we're doing more
+ # than checking.
+ if not checkOnly and not directory:
+ directory = config.directory
+
+ if principalID.startswith("/"):
+ raise ValueError("Can't resolve paths yet")
+
+ if checkOnly:
+ return None
+
+ if principalID.startswith("("):
+ try:
+ i = principalID.index(")")
+
+ if checkOnly:
+ return None
+
+ recordType = principalID[1:i]
+ shortName = principalID[i+1:]
+
+ if not recordType or not shortName or "(" in recordType:
+ raise ValueError()
+
+ return directory.principalCollection.principalForShortName(recordType, shortName)
+
+ except ValueError:
+ pass
+
+ if ":" in principalID:
+ if checkOnly:
+ return None
+
+ recordType, shortName = principalID.split(":", 1)
+
+ return directory.principalCollection.principalForShortName(recordType, shortName)
+
+ try:
+ guid = UUID(principalID)
+
+ if checkOnly:
+ return None
+
+ return directory.principalCollection.principalForUID(guid)
+ except ValueError:
+ pass
+
+ raise ValueError("Invalid principal identifier: %s" % (principalID,))
+
+def proxySubprincipal(principal, proxyType):
+ return principal.getChild("calendar-proxy-" + proxyType)
+
+ at inlineCallbacks
+def action_readProperty(resource, qname):
+ property = (yield resource.readProperty(qname, None))
+ print "%r on %s:" % (qname2sname(qname), resource)
+ print ""
+ print property.toxml()
+
+ at inlineCallbacks
+def action_listProxies(principal, *proxyTypes):
+ for proxyType in proxyTypes:
+ subPrincipal = proxySubprincipal(principal, proxyType)
+ if subPrincipal is None:
+ print "No %s proxies for %s" % (proxyType, principal)
+ continue
+
+ membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+ if membersProperty.children:
+ print "%s proxies for %s:" % (
+ {"read": "Read-only", "write": "Read/write"}[proxyType],
+ principal,
+ )
+ for member in membersProperty.children:
+ print " *", member
+ else:
+ print "No %s proxies for %s" % (proxyType, principal)
+
+ at inlineCallbacks
+def action_addProxy(principal, proxyType, *proxyIDs):
+ for proxyID in proxyIDs:
+ proxyPrincipal = principalForPrincipalID(proxyID)
+ (yield action_addProxyPrincipal(principal, proxyType, proxyPrincipal))
+
+ at inlineCallbacks
+def action_addProxyPrincipal(principal, proxyType, proxyPrincipal):
+ proxyURL = proxyPrincipal.url()
+
+ subPrincipal = proxySubprincipal(principal, proxyType)
+ if subPrincipal is None:
+ sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
+ return
+
+ membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+ for memberURL in membersProperty.children:
+ if str(memberURL) == proxyURL:
+ print "%s is already a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
+ break
+ else:
+ memberURLs = list(membersProperty.children)
+ memberURLs.append(davxml.HRef(proxyURL))
+ membersProperty = davxml.GroupMemberSet(*memberURLs)
+ (yield subPrincipal.writeProperty(membersProperty, None))
+ print "Added %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
+
+ proxyTypes = ["read", "write"]
+ proxyTypes.remove(proxyType)
+
+ (yield action_removeProxyPrincipal(principal, proxyPrincipal, proxyTypes=proxyTypes))
+
+ at inlineCallbacks
+def action_removeProxy(principal, *proxyIDs, **kwargs):
+ for proxyID in proxyIDs:
+ proxyPrincipal = principalForPrincipalID(proxyID)
+ (yield action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs))
+
+ at inlineCallbacks
+def action_removeProxyPrincipal(principal, proxyPrincipal, **kwargs):
+ proxyTypes = kwargs.get("proxyTypes", ("read", "write"))
+ for proxyType in proxyTypes:
+ proxyURL = proxyPrincipal.url()
+
+ subPrincipal = proxySubprincipal(principal, proxyType)
+ if subPrincipal is None:
+ sys.stderr.write("Unable to edit %s proxies for %s\n" % (proxyType, principal))
+ continue
+
+ membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+
+ memberURLs = [
+ m for m in membersProperty.children
+ if str(m) != proxyURL
+ ]
+
+ if len(memberURLs) == len(membersProperty.children):
+ # No change
+ continue
+
+ membersProperty = davxml.GroupMemberSet(*memberURLs)
+ (yield subPrincipal.writeProperty(membersProperty, None))
+ print "Removed %s as a %s proxy for %s" % (proxyPrincipal, proxyType, principal)
+
+ at inlineCallbacks
+def action_setAutoSchedule(principal, autoSchedule):
+ if autoSchedule and principal.record.recordType in ("users", "groups"):
+ print "Enabling auto-schedule for %s is not allowed." % (principal,)
+ else:
+ print "Setting auto-schedule to %s for %s" % (
+ { True: "true", False: "false" }[autoSchedule],
+ principal,
+ )
+ principal.setAutoSchedule(autoSchedule)
+
+ at inlineCallbacks
+def action_getAutoSchedule(principal):
+ autoSchedule = principal.getAutoSchedule()
+ print "Autoschedule for %s is %s" % (
+ principal,
+ { True: "true", False: "false" }[autoSchedule],
+ )
+
+def abort(msg, status=1):
+ sys.stdout.write("%s\n" % (msg,))
+ try:
+ reactor.stop()
+ except RuntimeError:
+ pass
+ sys.exit(status)
+
+if __name__ == "__main__":
+ main()
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/util.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,137 +0,0 @@
-##
-# Copyright (c) 2008-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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-__all__ = [
- "loadConfig",
- "getDirectory",
- "UsageError",
- "booleanArgument",
-]
-
-import os
-
-from twisted.python.reflect import namedClass
-
-import socket
-from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import augment, calendaruserproxy
-from twistedcaldav.directory.directory import DirectoryService
-
-def loadConfig(configFileName):
- if configFileName is None:
- configFileName = "/etc/caldav/caldavd.plist"
-
- if not os.path.isfile(configFileName):
- raise ConfigurationError("No config file: %s" % (configFileName,))
-
- config.loadConfig(configFileName)
-
- return config
-
-def getDirectory():
- BaseDirectoryService = namedClass(config.DirectoryService.type)
-
- class MyDirectoryService (BaseDirectoryService):
- def getPrincipalCollection(self):
- if not hasattr(self, "_principalCollection"):
- #
- # Instantiating a CalendarHomeProvisioningResource with a directory
- # will register it with the directory (still smells like a hack).
- #
- # We need that in order to locate calendar homes via the directory.
- #
- from twistedcaldav.static import CalendarHomeProvisioningFile
- CalendarHomeProvisioningFile(os.path.join(config.DocumentRoot, "calendars"), self, "/calendars/")
-
- from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
- self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
-
- return self._principalCollection
-
- def setPrincipalCollection(self, coll):
- # See principal.py line 237: self.directory.principalCollection = self
- pass
-
- principalCollection = property(getPrincipalCollection, setPrincipalCollection)
-
- def calendarHomeForRecord(self, record):
- principal = self.principalCollection.principalForRecord(record)
- if principal:
- try:
- return principal.calendarHome()
- except AttributeError:
- pass
- return None
-
- def calendarHomeForShortName(self, recordType, shortName):
- principal = self.principalCollection.principalForShortName(recordType, shortName)
- if principal:
- return principal.calendarHome()
- return None
-
- def principalForCalendarUserAddress(self, cua):
- return self.principalCollection.principalForCalendarUserAddress(cua)
-
- # Load augment/proxy db classes now
- augmentClass = namedClass(config.AugmentService.type)
- augment.AugmentService = augmentClass(**config.AugmentService.params)
-
- proxydbClass = namedClass(config.ProxyDBService.type)
- calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
-
- # Wait for directory service to become available
- directory = MyDirectoryService(**config.DirectoryService["params"])
-
- return directory
-
-class DummyDirectoryService (DirectoryService):
- realmName = ""
- baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
- def recordTypes(self): return ()
- def listRecords(self): return ()
- def recordWithShortName(self): return None
-
-class UsageError (StandardError):
- pass
-
-def booleanArgument(arg):
- if arg in ("true", "yes", "yup", "uh-huh", "1", "t", "y"):
- return True
- elif arg in ("false", "no", "nope", "nuh-uh", "0", "f", "n"):
- return False
- else:
- raise ValueError("Not a boolean: %s" % (arg,))
-
-
-
-
-
-def autoDisableMemcached(config):
- """
- If memcached is not running, set config.Memcached.ClientEnabled to False
- """
-
- if not config.Memcached.Pools.Default.ClientEnabled:
- return
-
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
- try:
- s.connect((config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port))
- s.close()
-
- except socket.error:
- config.Memcached.Pools.Default.ClientEnabled = False
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/calendarserver/tools/util.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/calendarserver/tools/util.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 2008-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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+ "loadConfig",
+ "getDirectory",
+ "UsageError",
+ "booleanArgument",
+]
+
+import os
+
+from twisted.python.reflect import namedClass
+
+import socket
+from twistedcaldav.config import config, ConfigurationError
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.directory import DirectoryService
+
+def loadConfig(configFileName):
+ if configFileName is None:
+ configFileName = "/etc/caldav/caldavd.plist"
+
+ if not os.path.isfile(configFileName):
+ raise ConfigurationError("No config file: %s" % (configFileName,))
+
+ config.loadConfig(configFileName)
+
+ return config
+
+def getDirectory():
+ BaseDirectoryService = namedClass(config.DirectoryService.type)
+
+ class MyDirectoryService (BaseDirectoryService):
+ def getPrincipalCollection(self):
+ if not hasattr(self, "_principalCollection"):
+ #
+ # Instantiating a CalendarHomeProvisioningResource with a directory
+ # will register it with the directory (still smells like a hack).
+ #
+ # We need that in order to locate calendar homes via the directory.
+ #
+ from twistedcaldav.static import CalendarHomeProvisioningFile
+ CalendarHomeProvisioningFile(os.path.join(config.DocumentRoot, "calendars"), self, "/calendars/")
+
+ from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+ self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
+
+ return self._principalCollection
+
+ def setPrincipalCollection(self, coll):
+ # See principal.py line 237: self.directory.principalCollection = self
+ pass
+
+ principalCollection = property(getPrincipalCollection, setPrincipalCollection)
+
+ def calendarHomeForRecord(self, record):
+ principal = self.principalCollection.principalForRecord(record)
+ if principal:
+ try:
+ return principal.calendarHome()
+ except AttributeError:
+ pass
+ return None
+
+ def calendarHomeForShortName(self, recordType, shortName):
+ principal = self.principalCollection.principalForShortName(recordType, shortName)
+ if principal:
+ return principal.calendarHome()
+ return None
+
+ def principalForCalendarUserAddress(self, cua):
+ return self.principalCollection.principalForCalendarUserAddress(cua)
+
+ # Load augment/proxy db classes now
+ augmentClass = namedClass(config.AugmentService.type)
+ augment.AugmentService = augmentClass(**config.AugmentService.params)
+
+ proxydbClass = namedClass(config.ProxyDBService.type)
+ calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+
+ # Wait for directory service to become available
+ directory = MyDirectoryService(**config.DirectoryService["params"])
+
+ return directory
+
+class DummyDirectoryService (DirectoryService):
+ realmName = ""
+ baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
+ def recordTypes(self): return ()
+ def listRecords(self): return ()
+ def recordWithShortName(self): return None
+
+class UsageError (StandardError):
+ pass
+
+def booleanArgument(arg):
+ if arg in ("true", "yes", "yup", "uh-huh", "1", "t", "y"):
+ return True
+ elif arg in ("false", "no", "nope", "nuh-uh", "0", "f", "n"):
+ return False
+ else:
+ raise ValueError("Not a boolean: %s" % (arg,))
+
+
+
+
+
+def autoDisableMemcached(config):
+ """
+ If memcached is not running, set config.Memcached.ClientEnabled to False
+ """
+
+ if not config.Memcached.Pools.Default.ClientEnabled:
+ return
+
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ try:
+ s.connect((config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port))
+ s.close()
+
+ except socket.error:
+ config.Memcached.Pools.Default.ClientEnabled = False
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts-test.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts-test.xml 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts-test.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -36,34 +36,24 @@
<guid>user%02d</guid>
<password>user%02d</password>
<name>User %02d</name>
- <cuaddr>mailto:user%02d at example.com</cuaddr>
</user>
<user repeat="10">
<uid>public%02d</uid>
<guid>public%02d</guid>
<password>public%02d</password>
<name>Public %02d</name>
- <cuaddr>mailto:public%02d at example.com</cuaddr>
</user>
<location repeat="10">
<uid>location%02d</uid>
<guid>location%02d</guid>
<password>location%02d</password>
<name>Room %02d</name>
- <auto-schedule/>
</location>
<resource repeat="10">
<uid>resource%02d</uid>
<guid>resource%02d</guid>
<password>resource%02d</password>
<name>Resource %02d</name>
- <auto-schedule/>
- <proxies>
- <member type="users">user01</member>
- </proxies>
- <read-only-proxies>
- <member type="users">user03</member>
- </read-only-proxies>
</resource>
<group>
<uid>group01</uid>
@@ -82,6 +72,5 @@
<members>
<member type="users">user01</member>
</members>
- <disable-calendar/>
</group>
</accounts>
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.dtd
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.dtd 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.dtd 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -19,16 +19,16 @@
<!ELEMENT accounts (user*, group*, resource*, location*) >
<!ATTLIST accounts realm CDATA "">
- <!ELEMENT user (uid, guid, password, name, cuaddr*, disable-calendar?)>
+ <!ELEMENT user (uid*, guid, password?, name?, email-address*)>
<!ATTLIST user repeat CDATA "1">
- <!ELEMENT group (uid, guid, password, name, members, cuaddr*, disable-calendar?)>
+ <!ELEMENT group (uid*, guid, password?, name?, members)>
<!ATTLIST group repeat CDATA "1">
- <!ELEMENT resource (uid, guid, password, name, cuaddr*, auto-schedule?, proxies?, read-only-proxies?)>
+ <!ELEMENT resource (uid*, guid, password?, name)>
<!ATTLIST resource repeat CDATA "1">
- <!ELEMENT location (uid, guid, password, name, cuaddr*, auto-schedule?, proxies?, read-only-proxies?)>
+ <!ELEMENT location (uid*, guid, password?, name)>
<!ATTLIST location repeat CDATA "1">
<!ELEMENT member (#PCDATA)>
@@ -38,10 +38,6 @@
<!ELEMENT guid (#PCDATA)>
<!ELEMENT password (#PCDATA)>
<!ELEMENT name (#PCDATA)>
- <!ELEMENT cuaddr (#PCDATA)>
+ <!ELEMENT email-address (#PCDATA)>
<!ELEMENT members (member*)>
- <!ELEMENT auto-schedule EMPTY>
- <!ELEMENT disable-calendar EMPTY>
- <!ELEMENT proxies (member*)>
- <!ELEMENT read-only-proxies (member*)>
>
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.xml 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/accounts.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
-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.
@@ -28,7 +28,6 @@
<uid>test</uid>
<password>test</password>
<name>Test User</name>
- <cuaddr>mailto:testuser at example.com</cuaddr>
</user>
<group>
<uid>users</uid>
@@ -42,10 +41,6 @@
<uid>mercury</uid>
<password>mercury</password>
<name>Mecury Conference Room, Building 1, 2nd Floor</name>
- <auto-schedule/>
- <proxies>
- <member type="users">test</member>
- </proxies>
</location>
</accounts>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments-test.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/augments-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments-test.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments-test.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE augments SYSTEM "augments.dtd">
+
+<augments>
+ <record>
+ <guid>admin</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>apprentice</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record repeat="99">
+ <guid>user%02d</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <cuaddr>mailto:user%02d at example.com</cuaddr>
+ </record>
+ <record repeat="10">
+ <guid>public%02d</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <cuaddr>mailto:public%02d at example.com</cuaddr>
+ </record>
+ <record repeat="10">
+ <guid>location%02d</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <auto-schedule>true</auto-schedule>
+ </record>
+ <record repeat="10">
+ <guid>resource%02d</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <auto-schedule>true</auto-schedule>
+ </record>
+ <record repeat="4">
+ <guid>group%02d</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>disabledgroup</guid>
+ <enable>false</enable>
+ </record>
+</augments>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments.dtd (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/augments.dtd)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments.dtd (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/augments.dtd 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,28 @@
+<!--
+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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<!ELEMENT augments (record*) >
+
+ <!ELEMENT record (guid, enable, hosted-at?, enable-calendar?, auto-schedule?, cuaddr*)>
+ <!ATTLIST record repeat CDATA "1">
+
+ <!ELEMENT guid (#PCDATA)>
+ <!ELEMENT enable (#PCDATA)>
+ <!ELEMENT hosted-at (#PCDATA)>
+ <!ELEMENT enable-calendar (#PCDATA)>
+ <!ELEMENT auto-schedule (#PCDATA)>
+ <!ELEMENT cuaddr (#PCDATA)>
+>
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd-test.plist 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd-test.plist 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
-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.
@@ -121,64 +121,101 @@
<dict>
<key>node</key>
<string>/Search</string>
- <key>requireComputerRecord</key>
- <true/>
+ <key>cacheTimeout</key>
+ <integer>30</integer>
</dict>
</dict>
-->
- <!-- Apache-style Basic Directory Service -->
- <!--
- <key>DirectoryService</key>
- <dict>
- <key>type</key>
- <string>twistedcaldav.directory.apache.BasicDirectoryService</string>
-
- <key>params</key>
+ <!--
+ Augment service
+
+ Augments for the directory service records to add calendar specific attributes.
+
+ A variety of augment services are available for use.
+ When using a partitioned server, a service that can be accessed from each host will be needed.
+ -->
+
+ <!-- XML File Augment Service -->
+ <key>AugmentService</key>
<dict>
- <key>userFile</key>
- <string>conf/basic</string>
- <key>groupFile</key>
- <string>conf/group</string>
+ <key>type</key>
+ <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>xmlFiles</key>
+ <array>
+ <string>conf/auth/augments-test.xml</string>
+ </array>
+ </dict>
</dict>
- </dict>
- -->
+
+ <!-- Sqlite Augment Service -->
+ <!--
+ <key>AugmentService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>dbpath</key>
+ <string>/etc/caldavd/augments.sqlite</string>
+ </dict>
+ </dict>
+ -->
- <!-- Apache-style Digest Directory Service -->
- <!--
- <key>DirectoryService</key>
- <dict>
- <key>type</key>
- <string>twistedcaldav.directory.apache.DigestDirectoryService</string>
-
- <key>params</key>
+ <!-- PostgreSQL Augment Service -->
+ <!--
+ <key>AugmentService</key>
<dict>
- <key>userFile</key>
- <string>conf/digest</string>
- <key>groupFile</key>
- <string>conf/group</string>
+ <key>type</key>
+ <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>host</key>
+ <string>localhost</string>
+ <key>database</key>
+ <string>augments</string>
+ </dict>
</dict>
- </dict>
- -->
+ -->
- <!-- SQL Directory Service -->
- <!--
- <key>DirectoryService</key>
- <dict>
- <key>type</key>
- <string>twistedcaldav.directory.sqldb.SQLDirectoryService</string>
-
- <key>params</key>
+ <!-- Sqlite ProxyDB Service -->
+ <key>ProxyDBService</key>
<dict>
- <key>dbParentPath</key>
- <string>twistedcaldav/test/data/</string>
- <key>xmlFile</key>
- <string>conf/accounts-test.xml</string>
+ <key>type</key>
+ <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>dbpath</key>
+ <string>data/proxies.sqlite</string>
+ </dict>
</dict>
- </dict>
- -->
+ <!-- PostgreSQL ProxyDB Service -->
+ <!--
+ <key>ProxyDBService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>host</key>
+ <string>localhost</string>
+ <key>database</key>
+ <string>proxies</string>
+ </dict>
+ </dict>
+ -->
+ <key>ProxyLoadFromFile</key>
+ <string>conf/auth/proxies-test.xml</string>
+
<!--
Special principals
@@ -415,6 +452,39 @@
</dict>
+ <!--
+ Server-to-server protocol
+ -->
+
+ <key>Scheduling</key>
+ <dict>
+
+ <!-- CalDAV protocol options -->
+ <key>CalDAV</key>
+ <dict>
+ <key>EmailDomain</key>
+ <string></string>
+ <key>HTTPDomain</key>
+ <string></string>
+ <key>AddressPatterns</key>
+ <array>
+ </array>
+ </dict>
+
+ <!-- iSchedule protocol options -->
+ <key>iSchedule</key>
+ <dict>
+ <key>Enabled</key>
+ <false/>
+ <key>AddressPatterns</key>
+ <array>
+ </array>
+ <key>Servers</key>
+ <string>conf/servertoserver-test.xml</string>
+ </dict>
+
+ </dict>
+
<!--
Notifications
-->
@@ -511,10 +581,6 @@
<!-- Support for Memcached -->
<key>Memcached</key>
<dict>
- <key>ServerEnabled</key>
- <true/>
- <key>ClientEnabled</key>
- <true/>
<key>MaxClients</key>
<integer>5</integer>
<key>memcached</key>
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd.plist 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/caldavd.plist 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
- 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.
@@ -120,13 +120,98 @@
<dict>
<key>node</key>
<string>/Search</string>
- <key>requireComputerRecord</key>
- <true/>
+ <key>cacheTimeout</key>
+ <integer>30</integer>
</dict>
</dict>
+ <!--
+ Augment service
+ Augments for the directory service records to add calendar specific attributes.
+
+ A variety of augment services are available for use.
+ When using a partitioned server, a service that can be accessed from each host will be needed.
+ -->
+
+ <!-- XML File Augment Service -->
+ <key>AugmentService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>xmlFiles</key>
+ <array>
+ <string>/etc/caldavd/augments.xml</string>
+ </array>
+ </dict>
+ </dict>
+
+ <!-- Sqlite Augment Service -->
<!--
+ <key>AugmentService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.augment.AugmentSqliteDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>dbpath</key>
+ <string>/etc/caldavd/augments.sqlite</string>
+ </dict>
+ </dict>
+ -->
+
+ <!-- PostgreSQL Augment Service -->
+ <!--
+ <key>AugmentService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>host</key>
+ <string>localhost</string>
+ <key>database</key>
+ <string>augments</string>
+ </dict>
+ </dict>
+ -->
+
+ <!-- Sqlite ProxyDB Service -->
+ <key>ProxyDBService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>dbpath</key>
+ <string>/etc/caldavd/proxies.sqlite</string>
+ </dict>
+ </dict>
+
+ <!-- PostgreSQL ProxyDB Service -->
+ <!--
+ <key>ProxyDBService</key>
+ <dict>
+ <key>type</key>
+ <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
+
+ <key>params</key>
+ <dict>
+ <key>host</key>
+ <string>localhost</string>
+ <key>database</key>
+ <string>proxies</string>
+ </dict>
+ </dict>
+ -->
+
+ <!--
Special principals
These principals are granted special access and/or perform
@@ -281,6 +366,40 @@
<!--
+ Server-to-server protocol
+ -->
+
+ <key>Scheduling</key>
+ <dict>
+
+ <!-- CalDAV protocol options -->
+ <key>CalDAV</key>
+ <dict>
+ <key>EmailDomain</key>
+ <string></string>
+ <key>HTTPDomain</key>
+ <string></string>
+ <key>AddressPatterns</key>
+ <array>
+ </array>
+ </dict>
+
+ <!-- iSchedule protocol options -->
+ <key>iSchedule</key>
+ <dict>
+ <key>Enabled</key>
+ <false/>
+ <key>AddressPatterns</key>
+ <array>
+ </array>
+ <key>Servers</key>
+ <string>/etc/caldavd/servertoserver.xml</string>
+ </dict>
+
+ </dict>
+
+
+ <!--
Notifications
-->
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions-test.plist (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/partitions-test.plist)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions-test.plist (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions-test.plist 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Copyright (c) 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.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>partitions</key>
+ <array>
+ <dict>
+ <key>uid</key>
+ <string>00001</string>
+ <key>url</key>
+ <string>http://localhost:8008</string>
+ </dict>
+ <dict>
+ <key>uid</key>
+ <string>00002</string>
+ <key>url</key>
+ <string>http://localhost:8108</string>
+ </dict>
+ </array>
+</dict>
+</plist>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions.plist (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/partitions.plist)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions.plist (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/partitions.plist 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ Copyright (c) 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.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>partitions</key>
+ <array>
+ <!--
+ <dict>
+ <key>uid</key>
+ <string>00001</string>
+ <key>url</key>
+ <string>http://localhost:8008</string>
+ </dict>
+ -->
+ </array>
+</dict>
+</plist>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies-test.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/proxies-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies-test.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies-test.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE proxies SYSTEM "proxies.dtd">
+
+<proxies>
+ <record repeat="10">
+ <guid>resource%02d</guid>
+ <proxies>
+ <member>user01</member>
+ </proxies>
+ <read-only-proxies>
+ <member>user03</member>
+ </read-only-proxies>
+ </record>
+</proxies>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies.dtd (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/proxies.dtd)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies.dtd (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/proxies.dtd 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,26 @@
+<!--
+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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<!ELEMENT proxies (record*) >
+
+ <!ELEMENT record (guid, proxies?, read-only-proxies?)>
+ <!ATTLIST record repeat CDATA "1">
+
+ <!ELEMENT guid (#PCDATA)>
+ <!ELEMENT proxies (member*)>
+ <!ELEMENT read-only-proxies (member*)>
+ <!ELEMENT member (#PCDATA)>
+>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver-test.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/servertoserver-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver-test.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver-test.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2007 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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE servers SYSTEM "servertoserver.dtd">
+
+<servers>
+ <server>
+ <uri>https://localhost:8543/inbox</uri>
+ <allow-requests-from/>
+ <allow-requests-to/>
+ <domains>
+ <domain>example.org</domain>
+ </domains>
+ <hosts>
+ <host>127.0.0.1</host>
+ </hosts>
+ </server>
+</servers>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver.dtd (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/conf/servertoserver.dtd)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver.dtd (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/conf/servertoserver.dtd 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,33 @@
+<!--
+Copyright (c) 2006-2007 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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<!ELEMENT servers (server*) >
+
+ <!ELEMENT server (uri, authentication?, allow-requests-from, allow-requests-to, domains?, hosts?) >
+
+ <!ELEMENT uri (#PCDATA) >
+ <!ELEMENT authentication (user, password) >
+ <!ATTLIST authentication type (basic) "">
+ <!ELEMENT user (#PCDATA) >
+ <!ELEMENT password (#PCDATA) >
+
+ <!ELEMENT allow-requests-from EMPTY >
+ <!ELEMENT allow-requests-to EMPTY >
+ <!ELEMENT domains (domain*) >
+ <!ELEMENT domain (#PCDATA) >
+ <!ELEMENT hosts (host*) >
+ <!ELEMENT host (#PCDATA) >
+
\ No newline at end of file
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/lib-patches/Twisted/twisted.web2.client.http.patch (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/lib-patches/Twisted/twisted.web2.client.http.patch)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/lib-patches/Twisted/twisted.web2.client.http.patch (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/lib-patches/Twisted/twisted.web2.client.http.patch 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,26 @@
+Index: twisted/web2/client/http.py
+===================================================================
+--- twisted/web2/client/http.py (revision 19773)
++++ twisted/web2/client/http.py (working copy)
+@@ -127,15 +127,16 @@
+
+ def _error(self, err):
+ self.abortParse()
+- self.responseDefer.errback(err)
++ if hasattr(self, 'stream') and self.stream is not None:
++ self.stream.finish(err)
++ else:
++ self.responseDefer.errback(err)
+
+ def _abortWithError(self, errcode, text):
+- self.abortParse()
+- self.responseDefer.errback(ProtocolError(text))
++ self._error(ProtocolError(text))
+
+ def connectionLost(self, reason):
+- ### FIXME!
+- pass
++ self._error(reason)
+
+ def gotInitialLine(self, initialLine):
+ parts = initialLine.split(' ', 2)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/memcacheclient.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/memcacheclient.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/memcacheclient.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -124,7 +124,7 @@
pickler=pickle.Pickler, unpickler=pickle.Unpickler,
pload=None, pid=None):
- if config.Memcached.ClientEnabled:
+ if config.Memcached.Pools.Default.ClientEnabled:
return Client(servers, debug=debug, pickleProtocol=pickleProtocol,
pickler=pickler, unpickler=unpickler, pload=pload, pid=pid)
elif cls.allowTestCache:
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/run
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/run 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/run 2009-12-11 18:41:21 UTC (rev 4857)
@@ -618,7 +618,7 @@
#
libevent="${top}/libevent-1.4.8-stable"
- www_get "libevent" "${libevent}" http://monkey.org/~provos/libevent-1.4.8-stable.tar.gz
+ www_get "libevent" "${libevent}" http://monkey.org/~provos/libevent-1.4.8-stable.tar.gz;
if "${do_setup}" && (
"${force_setup}" || [ ! -d "${libevent}/_root" ]
@@ -657,7 +657,43 @@
export PATH="${PATH}:${top}/memcached-1.2.6/_root/bin";
fi;
+if ! type psql >& /dev/null; then
+ #
+ # postgresql
+ #
+ postgresql="${top}/postgresql-8.4.1"
+
+ www_get "postgresql" "${postgresql}" http://wwwmaster.postgresql.org/redir/198/h/source/v8.4.1/postgresql-8.4.1.tar.bz2;
+
+ if "${do_setup}" && (
+ "${force_setup}" || [ ! -d "${postgresql}/_root" ]
+ ); then
+ echo "";
+ echo "Building postgresql...";
+ cd "${postgresql}";
+ ./configure --prefix="${postgresql}/_root";
+ make;
+ make install;
+ fi;
+
+ export PATH="${PATH:-}:${postgresql}/_root/bin";
+fi;
+
#
+# PyGreSQL
+#
+
+if ! py_have_module pgdb; then
+ PyGreSQL="${top}/PyGreSQL-4.0";
+
+ www_get "PyGreSQL" "${PyGreSQL}" ftp://ftp.pygresql.org/pub/distrib/PyGreSQL.tgz;
+ py_build "PyGreSQL" "${PyGreSQL}" false;
+ py_install "PyGreSQL" "${PyGreSQL}";
+
+ export PYTHONPATH="${PYTHONPATH}:${PyGreSQL}/build/${py_platform_libdir}";
+fi;
+
+#
# Twisted
#
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,19 +0,0 @@
-##
-# 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to the Twisted Framework.
-"""
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,19 @@
+##
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to the Twisted Framework.
+"""
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.python.
-"""
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.python.
+"""
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/log.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,49 +0,0 @@
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.python.log.
-"""
-
-from sys import stdout, stderr
-
-from twisted.python.log import addObserver, removeObserver
-
-class StandardIOObserver (object):
- """
- Log observer that writes to standard I/O.
- """
- def emit(self, eventDict):
- text = None
-
- if eventDict["isError"]:
- output = stderr
- if "failure" in eventDict:
- text = eventDict["failure"].getTraceback()
- else:
- output = stdout
-
- if not text:
- text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
-
- output.write(text)
- output.flush()
-
- def start(self):
- addObserver(self.emit)
-
- def stop(self):
- removeObserver(self.emit)
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/python/log.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/python/log.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,49 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.python.log.
+"""
+
+from sys import stdout, stderr
+
+from twisted.python.log import addObserver, removeObserver
+
+class StandardIOObserver (object):
+ """
+ Log observer that writes to standard I/O.
+ """
+ def emit(self, eventDict):
+ text = None
+
+ if eventDict["isError"]:
+ output = stderr
+ if "failure" in eventDict:
+ text = eventDict["failure"].getTraceback()
+ else:
+ output = stdout
+
+ if not text:
+ text = " ".join([str(m) for m in eventDict["message"]]) + "\n"
+
+ output.write(text)
+ output.flush()
+
+ def start(self):
+ addObserver(self.emit)
+
+ def stop(self):
+ removeObserver(self.emit)
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.web2
-"""
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.web2
+"""
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.web2.dav
-"""
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.web2.dav
+"""
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/davxml.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,122 +0,0 @@
-##
-# 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Extensions to twisted.web2.dav.davxml
-"""
-
-__all__ = [
- "sname2qname",
- "qname2sname",
- "ErrorResponse",
-]
-
-from twisted.web2.http import Response
-from twisted.web2.dav.http import ErrorResponse as SuperErrorResponse
-from twisted.web2.dav.davxml import twisted_dav_namespace, WebDAVTextElement
-from twisted.web2.dav.davxml import WebDAVUnknownElement, Error
-from twisted.web2.http_headers import MimeType
-
-
-def sname2qname(sname):
- """
- Convert an sname into a qname.
-
- That is, parse a property name string (eg: C{"{DAV:}displayname"})
- into a tuple (eg: C{("DAV:", "displayname")}).
-
- @raise ValueError is input is not valid. Note, however, that this
- function does not attempt to fully validate C{sname}.
- """
- def raiseIf(condition):
- if condition:
- raise ValueError("Invalid sname: %s" % (sname,))
-
- raiseIf(not sname.startswith("{"))
-
- try:
- i = sname.index("}")
- except ValueError:
- raiseIf(True)
-
- namespace = sname[1:i]
- name = sname [i+1:]
-
- raiseIf("{" in namespace or not name)
-
- return namespace, name
-
-def qname2sname(qname):
- """
- Convert a qname into an sname.
- """
- try:
- return "{%s}%s" % qname
- except TypeError:
- raise ValueError("Invalid qname: %r" % (qname,))
-
-
-
-
-
-class ErrorDescription(WebDAVTextElement):
- """
- The human-readable description of a failed precondition
- """
- namespace = twisted_dav_namespace
- name = "error-description"
- protected = True
-
-
-class ErrorResponse(SuperErrorResponse):
- """
- A L{Response} object which contains a status code and a L{davxml.Error}
- element.
- Renders itself as a DAV:error XML document.
- """
- error = None
-
- def __init__(self, code, error, description=None):
- """
- @param code: a response code.
- @param error: an L{davxml.WebDAVElement} identifying the error, or a
- tuple C{(namespace, name)} with which to create an empty element
- denoting the error. (The latter is useful in the case of
- preconditions ans postconditions, not all of which have defined
- XML element classes.)
- @param description: an optional string that, if present, will get
- wrapped in a (twisted_dav_namespace, error-description) element.
- """
- if type(error) is tuple:
- xml_namespace, xml_name = error
- error = WebDAVUnknownElement()
- error.namespace = xml_namespace
- error.name = xml_name
-
- if description:
- output = Error(error, ErrorDescription(description)).toxml()
- else:
- output = Error(error).toxml()
-
- Response.__init__(self, code=code, stream=output)
-
- self.headers.setHeader("content-type", MimeType("text", "xml"))
-
- self.error = error
-
- def __repr__(self):
- return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
-
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twext/web2/dav/davxml.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twext/web2/dav/davxml.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,122 @@
+##
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Extensions to twisted.web2.dav.davxml
+"""
+
+__all__ = [
+ "sname2qname",
+ "qname2sname",
+ "ErrorResponse",
+]
+
+from twisted.web2.http import Response
+from twisted.web2.dav.http import ErrorResponse as SuperErrorResponse
+from twisted.web2.dav.davxml import twisted_dav_namespace, WebDAVTextElement
+from twisted.web2.dav.davxml import WebDAVUnknownElement, Error
+from twisted.web2.http_headers import MimeType
+
+
+def sname2qname(sname):
+ """
+ Convert an sname into a qname.
+
+ That is, parse a property name string (eg: C{"{DAV:}displayname"})
+ into a tuple (eg: C{("DAV:", "displayname")}).
+
+ @raise ValueError is input is not valid. Note, however, that this
+ function does not attempt to fully validate C{sname}.
+ """
+ def raiseIf(condition):
+ if condition:
+ raise ValueError("Invalid sname: %s" % (sname,))
+
+ raiseIf(not sname.startswith("{"))
+
+ try:
+ i = sname.index("}")
+ except ValueError:
+ raiseIf(True)
+
+ namespace = sname[1:i]
+ name = sname [i+1:]
+
+ raiseIf("{" in namespace or not name)
+
+ return namespace, name
+
+def qname2sname(qname):
+ """
+ Convert a qname into an sname.
+ """
+ try:
+ return "{%s}%s" % qname
+ except TypeError:
+ raise ValueError("Invalid qname: %r" % (qname,))
+
+
+
+
+
+class ErrorDescription(WebDAVTextElement):
+ """
+ The human-readable description of a failed precondition
+ """
+ namespace = twisted_dav_namespace
+ name = "error-description"
+ protected = True
+
+
+class ErrorResponse(SuperErrorResponse):
+ """
+ A L{Response} object which contains a status code and a L{davxml.Error}
+ element.
+ Renders itself as a DAV:error XML document.
+ """
+ error = None
+
+ def __init__(self, code, error, description=None):
+ """
+ @param code: a response code.
+ @param error: an L{davxml.WebDAVElement} identifying the error, or a
+ tuple C{(namespace, name)} with which to create an empty element
+ denoting the error. (The latter is useful in the case of
+ preconditions ans postconditions, not all of which have defined
+ XML element classes.)
+ @param description: an optional string that, if present, will get
+ wrapped in a (twisted_dav_namespace, error-description) element.
+ """
+ if type(error) is tuple:
+ xml_namespace, xml_name = error
+ error = WebDAVUnknownElement()
+ error.namespace = xml_namespace
+ error.name = xml_name
+
+ if description:
+ output = Error(error, ErrorDescription(description)).toxml()
+ else:
+ output = Error(error).toxml()
+
+ Response.__init__(self, code=code, stream=output)
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ self.error = error
+
+ def __repr__(self):
+ return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
+
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cache.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cache.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cache.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -28,7 +28,7 @@
from twisted.web2.stream import MemoryStream
from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.memcachepool import CachePoolUserMixIn
+from twistedcaldav.memcachepool import CachePoolUserMixIn, defaultCachePool
from twistedcaldav.config import config
@@ -65,11 +65,11 @@
class MemcacheChangeNotifier(LoggingMixIn, CachePoolUserMixIn):
- def __init__(self, resource, cachePool=None):
+ def __init__(self, resource, cachePool=None, cacheHandle="Default"):
self._resource = resource
self._cachePool = cachePool
+ self._cachePoolHandle = cacheHandle
-
def _newCacheToken(self):
return str(uuid.uuid4())
@@ -171,7 +171,7 @@
self._cachePool = cachePool
- def _tokenForURI(self, uri):
+ def _tokenForURI(self, uri, cachePoolHandle=None):
"""
Get a property store for the given C{uri}.
@@ -179,13 +179,16 @@
@return: A C{str} representing the token for the URI.
"""
- return self.getCachePool().get('cacheToken:%s' % (uri,))
+ if cachePoolHandle:
+ return defaultCachePool(cachePoolHandle).get('cacheToken:%s' % (uri,))
+ else:
+ return self.getCachePool().get('cacheToken:%s' % (uri,))
def _getTokens(self, request):
def _tokensForURIs((pURI, rURI)):
tokens = []
- d1 = self._tokenForURI(pURI)
+ d1 = self._tokenForURI(pURI, "PrincipalToken")
d1.addCallback(tokens.append)
d1.addCallback(lambda _ign: self._getRecordForURI(pURI, request))
d1.addCallback(lambda dToken: tokens.append(hash(dToken)))
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,16 +0,0 @@
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,16 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/pool.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,389 +0,0 @@
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-__all__ = [
- "installPools",
- "installPool",
- "getHTTPClientPool",
-]
-
-from twisted.internet.address import IPv4Address
-from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
-from twisted.internet.error import ConnectionLost, ConnectionDone, ConnectError
-from twisted.internet.protocol import ClientFactory
-from twisted.internet.ssl import DefaultOpenSSLContextFactory
-from twisted.web2 import responsecode
-from twisted.web2.client.http import HTTPClientProtocol
-from twisted.web2.http import StatusResponse, HTTPError
-from twistedcaldav.log import LoggingMixIn
-import OpenSSL
-import urlparse
-
-class PooledHTTPClientFactory(ClientFactory, LoggingMixIn):
- """
- A client factory for HTTPClient that notifies a pool of it's state. It the connection
- fails in the middle of a request it will retry the request.
-
- @ivar protocol: The current instance of our protocol that we pass
- to our connectionPool.
- @ivar connectionPool: A managing connection pool that we notify of events.
- """
- protocol = HTTPClientProtocol
- connectionPool = None
-
- def __init__(self, reactor):
- self.reactor = reactor
- self.instance = None
- self.onConnect = Deferred()
- self.afterConnect = Deferred()
-
- def clientConnectionLost(self, connector, reason):
- """
- Notify the connectionPool that we've lost our connection.
- """
-
- if hasattr(self, "afterConnect"):
- self.reactor.callLater(0, self.afterConnect.errback, reason)
- del self.afterConnect
-
- if self.connectionPool.shutdown_requested:
- # The reactor is stopping; don't reconnect
- return
-
- def clientConnectionFailed(self, connector, reason):
- """
- Notify the connectionPool that we're unable to connect
- """
- if hasattr(self, "onConnect"):
- self.reactor.callLater(0, self.onConnect.errback, reason)
- del self.onConnect
- elif hasattr(self, "afterConnect"):
- self.reactor.callLater(0, self.afterConnect.errback, reason)
- del self.afterConnect
-
- def buildProtocol(self, addr):
- self.instance = self.protocol()
- self.reactor.callLater(0, self.onConnect.callback, self.instance)
- del self.onConnect
- return self.instance
-
-class HTTPClientPool(LoggingMixIn):
- """
- A connection pool for HTTPClientProtocol instances.
-
- @ivar clientFactory: The L{ClientFactory} implementation that will be used
- for each protocol.
-
- @ivar _maxClients: A C{int} indicating the maximum number of clients.
- @ivar _serverAddress: An L{IAddress} provider indicating the server to
- connect to. (Only L{IPv4Address} currently supported.)
- @ivar _reactor: The L{IReactorTCP} provider used to initiate new
- connections.
-
- @ivar _busyClients: A C{set} that contains all currently busy clients.
- @ivar _freeClients: A C{set} that contains all currently free clients.
- @ivar _pendingConnects: A C{int} indicating how many connections are in
- progress.
- """
- clientFactory = PooledHTTPClientFactory
- maxRetries = 2
-
- def __init__(self, name, scheme, serverAddress, maxClients=5, reactor=None):
- """
- @param serverAddress: An L{IPv4Address} indicating the server to
- connect to.
- @param maxClients: A C{int} indicating the maximum number of clients.
- @param reactor: An L{IReactorTCP{ provider used to initiate new
- connections.
- """
-
- self._name = name
- self._scheme = scheme
- self._serverAddress = serverAddress
- self._maxClients = maxClients
-
- if reactor is None:
- from twisted.internet import reactor
- self._reactor = reactor
-
- self.shutdown_deferred = None
- self.shutdown_requested = False
- reactor.addSystemEventTrigger('before', 'shutdown', self._shutdownCallback)
-
- self._busyClients = set([])
- self._freeClients = set([])
- self._pendingConnects = 0
- self._pendingRequests = []
-
- def _isIdle(self):
- return (
- len(self._busyClients) == 0 and
- len(self._pendingRequests) == 0 and
- self._pendingConnects == 0
- )
-
- def _shutdownCallback(self):
- self.shutdown_requested = True
- if self._isIdle():
- return None
- self.shutdown_deferred = Deferred()
- return self.shutdown_deferred
-
- def _newClientConnection(self):
- """
- Create a new client connection.
-
- @return: A L{Deferred} that fires with the L{IProtocol} instance.
- """
- self.log_debug("Initiating new client connection to: %s" % (self._serverAddress,))
- self._logClientStats()
-
- self._pendingConnects += 1
-
- factory = self.clientFactory(self._reactor)
- factory.connectionPool = self
-
- if self._scheme == "https":
- from twistedcaldav.config import config
- context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
- self._reactor.connectSSL(self._serverAddress.host, self._serverAddress.port, factory, context)
- elif self._scheme == "http":
- self._reactor.connectTCP(self._serverAddress.host, self._serverAddress.port, factory)
- else:
- raise ValueError("URL scheme for client pool not supported")
-
- def _doneOK(client):
- self._pendingConnects -= 1
-
- def _goneClientAfterError(f, client):
- f.trap(ConnectionLost, ConnectionDone, ConnectError)
- self.clientGone(client)
-
- d2 = factory.afterConnect
- d2.addErrback(_goneClientAfterError, client)
- return client
-
- def _doneError(result):
- self._pendingConnects -= 1
- return result
-
- d = factory.onConnect
- d.addCallbacks(_doneOK, _doneError)
-
- return d
-
- def _performRequestOnClient(self, client, request, *args, **kwargs):
- """
- Perform the given request on the given client.
-
- @param client: A L{PooledMemCacheProtocol} that will be used to perform
- the given request.
-
- @param command: A C{str} representing an attribute of
- L{MemCacheProtocol}.
- @parma args: Any positional arguments that should be passed to
- C{command}.
- @param kwargs: Any keyword arguments that should be passed to
- C{command}.
-
- @return: A L{Deferred} that fires with the result of the given command.
- """
-
- def _freeClientAfterRequest(result):
- self.clientFree(client)
- return result
-
- def _goneClientAfterError(result):
- self.clientGone(client)
- return result
-
- self.clientBusy(client)
- d = client.submitRequest(request, closeAfter=False)
- d.addCallbacks(_freeClientAfterRequest, _goneClientAfterError)
- return d
-
- @inlineCallbacks
- def submitRequest(self, request, *args, **kwargs):
- """
- Select an available client and perform the given request on it.
-
- @param command: A C{str} representing an attribute of
- L{MemCacheProtocol}.
- @parma args: Any positional arguments that should be passed to
- C{command}.
- @param kwargs: Any keyword arguments that should be passed to
- C{command}.
-
- @return: A L{Deferred} that fires with the result of the given command.
- """
-
- # Try this maxRetries times
- for ctr in xrange(self.maxRetries + 1):
- try:
- response = (yield self._submitRequest(request, args, kwargs))
- except (ConnectionLost, ConnectionDone, ConnectError), e:
- self.log_error("HTTP pooled client connection error (attempt: %d) - retrying: %s" % (ctr+1, e,))
- continue
-
- # TODO: find the proper cause of these assertions and fix
- except (AssertionError,), e:
- self.log_error("HTTP pooled client connection assertion error (attempt: %d) - retrying: %s" % (ctr+1, e,))
- continue
- else:
- returnValue(response)
- else:
- self.log_error("HTTP pooled client connection error - exhausted retry attempts.")
- raise HTTPError(StatusResponse(responsecode.BAD_GATEWAY, "Could not connect to HTTP pooled client host."))
-
- def _submitRequest(self, request, *args, **kwargs):
- """
- Select an available client and perform the given request on it.
-
- @param command: A C{str} representing an attribute of
- L{MemCacheProtocol}.
- @parma args: Any positional arguments that should be passed to
- C{command}.
- @param kwargs: Any keyword arguments that should be passed to
- C{command}.
-
- @return: A L{Deferred} that fires with the result of the given command.
- """
-
- if len(self._freeClients) > 0:
- d = self._performRequestOnClient(self._freeClients.pop(), request, *args, **kwargs)
-
- elif len(self._busyClients) + self._pendingConnects >= self._maxClients:
- d = Deferred()
- self._pendingRequests.append((d, request, args, kwargs))
- self.log_debug("Request queued: %s, %r, %r" % (request, args, kwargs))
- self._logClientStats()
-
- else:
- d = self._newClientConnection()
- d.addCallback(self._performRequestOnClient, request, *args, **kwargs)
-
- return d
-
- def _logClientStats(self):
- self.log_debug("Clients #free: %d, #busy: %d, "
- "#pending: %d, #queued: %d" % (
- len(self._freeClients),
- len(self._busyClients),
- self._pendingConnects,
- len(self._pendingRequests)))
-
- def clientGone(self, client):
- """
- Notify that the given client is to be removed from the pool completely.
-
- @param client: An instance of L{PooledMemCacheProtocol}.
- """
- if client in self._busyClients:
- self._busyClients.remove(client)
-
- elif client in self._freeClients:
- self._freeClients.remove(client)
-
- self.log_debug("Removed client: %r" % (client,))
- self._logClientStats()
-
- self._processPending()
-
- def clientBusy(self, client):
- """
- Notify that the given client is being used to complete a request.
-
- @param client: An instance of C{self.clientFactory}
- """
-
- if client in self._freeClients:
- self._freeClients.remove(client)
-
- self._busyClients.add(client)
-
- self.log_debug("Busied client: %r" % (client,))
- self._logClientStats()
-
- def clientFree(self, client):
- """
- Notify that the given client is free to handle more requests.
-
- @param client: An instance of C{self.clientFactory}
- """
- if client in self._busyClients:
- self._busyClients.remove(client)
-
- self._freeClients.add(client)
-
- if self.shutdown_deferred and self._isIdle():
- self.shutdown_deferred.callback(None)
-
- self.log_debug("Freed client: %r" % (client,))
- self._logClientStats()
-
- self._processPending()
-
- def _processPending(self):
- if len(self._pendingRequests) > 0:
- d, request, args, kwargs = self._pendingRequests.pop(0)
-
- self.log_debug("Performing Queued Request: %s, %r, %r" % (
- request, args, kwargs))
- self._logClientStats()
-
- _ign_d = self._submitRequest(request, *args, **kwargs)
-
- _ign_d.addCallbacks(d.callback, d.errback)
-
- def suggestMaxClients(self, maxClients):
- """
- Suggest the maximum number of concurrently connected clients.
-
- @param maxClients: A C{int} indicating how many client connections we
- should keep open.
- """
- self._maxClients = maxClients
-
-_clientPools = {} # Maps a host:port to a pool object
-
-def installPools(hosts, maxClients=5, reactor=None):
-
- for name, url in hosts:
- installPool(
- name,
- url,
- maxClients,
- reactor,
- )
-
-def installPool(name, url, maxClients=5, reactor=None):
-
- parsedURL = urlparse.urlparse(url)
- pool = HTTPClientPool(
- name,
- parsedURL.scheme,
- IPv4Address(
- "TCP",
- parsedURL.hostname,
- parsedURL.port,
- ),
- maxClients,
- reactor,
- )
- _clientPools[name] = pool
-
-def getHTTPClientPool(name):
- return _clientPools[name]
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/pool.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/pool.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,389 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+ "installPools",
+ "installPool",
+ "getHTTPClientPool",
+]
+
+from twisted.internet.address import IPv4Address
+from twisted.internet.defer import Deferred, inlineCallbacks, returnValue
+from twisted.internet.error import ConnectionLost, ConnectionDone, ConnectError
+from twisted.internet.protocol import ClientFactory
+from twisted.internet.ssl import DefaultOpenSSLContextFactory
+from twisted.web2 import responsecode
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.http import StatusResponse, HTTPError
+from twistedcaldav.log import LoggingMixIn
+import OpenSSL
+import urlparse
+
+class PooledHTTPClientFactory(ClientFactory, LoggingMixIn):
+ """
+ A client factory for HTTPClient that notifies a pool of it's state. It the connection
+ fails in the middle of a request it will retry the request.
+
+ @ivar protocol: The current instance of our protocol that we pass
+ to our connectionPool.
+ @ivar connectionPool: A managing connection pool that we notify of events.
+ """
+ protocol = HTTPClientProtocol
+ connectionPool = None
+
+ def __init__(self, reactor):
+ self.reactor = reactor
+ self.instance = None
+ self.onConnect = Deferred()
+ self.afterConnect = Deferred()
+
+ def clientConnectionLost(self, connector, reason):
+ """
+ Notify the connectionPool that we've lost our connection.
+ """
+
+ if hasattr(self, "afterConnect"):
+ self.reactor.callLater(0, self.afterConnect.errback, reason)
+ del self.afterConnect
+
+ if self.connectionPool.shutdown_requested:
+ # The reactor is stopping; don't reconnect
+ return
+
+ def clientConnectionFailed(self, connector, reason):
+ """
+ Notify the connectionPool that we're unable to connect
+ """
+ if hasattr(self, "onConnect"):
+ self.reactor.callLater(0, self.onConnect.errback, reason)
+ del self.onConnect
+ elif hasattr(self, "afterConnect"):
+ self.reactor.callLater(0, self.afterConnect.errback, reason)
+ del self.afterConnect
+
+ def buildProtocol(self, addr):
+ self.instance = self.protocol()
+ self.reactor.callLater(0, self.onConnect.callback, self.instance)
+ del self.onConnect
+ return self.instance
+
+class HTTPClientPool(LoggingMixIn):
+ """
+ A connection pool for HTTPClientProtocol instances.
+
+ @ivar clientFactory: The L{ClientFactory} implementation that will be used
+ for each protocol.
+
+ @ivar _maxClients: A C{int} indicating the maximum number of clients.
+ @ivar _serverAddress: An L{IAddress} provider indicating the server to
+ connect to. (Only L{IPv4Address} currently supported.)
+ @ivar _reactor: The L{IReactorTCP} provider used to initiate new
+ connections.
+
+ @ivar _busyClients: A C{set} that contains all currently busy clients.
+ @ivar _freeClients: A C{set} that contains all currently free clients.
+ @ivar _pendingConnects: A C{int} indicating how many connections are in
+ progress.
+ """
+ clientFactory = PooledHTTPClientFactory
+ maxRetries = 2
+
+ def __init__(self, name, scheme, serverAddress, maxClients=5, reactor=None):
+ """
+ @param serverAddress: An L{IPv4Address} indicating the server to
+ connect to.
+ @param maxClients: A C{int} indicating the maximum number of clients.
+ @param reactor: An L{IReactorTCP{ provider used to initiate new
+ connections.
+ """
+
+ self._name = name
+ self._scheme = scheme
+ self._serverAddress = serverAddress
+ self._maxClients = maxClients
+
+ if reactor is None:
+ from twisted.internet import reactor
+ self._reactor = reactor
+
+ self.shutdown_deferred = None
+ self.shutdown_requested = False
+ reactor.addSystemEventTrigger('before', 'shutdown', self._shutdownCallback)
+
+ self._busyClients = set([])
+ self._freeClients = set([])
+ self._pendingConnects = 0
+ self._pendingRequests = []
+
+ def _isIdle(self):
+ return (
+ len(self._busyClients) == 0 and
+ len(self._pendingRequests) == 0 and
+ self._pendingConnects == 0
+ )
+
+ def _shutdownCallback(self):
+ self.shutdown_requested = True
+ if self._isIdle():
+ return None
+ self.shutdown_deferred = Deferred()
+ return self.shutdown_deferred
+
+ def _newClientConnection(self):
+ """
+ Create a new client connection.
+
+ @return: A L{Deferred} that fires with the L{IProtocol} instance.
+ """
+ self.log_debug("Initiating new client connection to: %s" % (self._serverAddress,))
+ self._logClientStats()
+
+ self._pendingConnects += 1
+
+ factory = self.clientFactory(self._reactor)
+ factory.connectionPool = self
+
+ if self._scheme == "https":
+ from twistedcaldav.config import config
+ context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
+ self._reactor.connectSSL(self._serverAddress.host, self._serverAddress.port, factory, context)
+ elif self._scheme == "http":
+ self._reactor.connectTCP(self._serverAddress.host, self._serverAddress.port, factory)
+ else:
+ raise ValueError("URL scheme for client pool not supported")
+
+ def _doneOK(client):
+ self._pendingConnects -= 1
+
+ def _goneClientAfterError(f, client):
+ f.trap(ConnectionLost, ConnectionDone, ConnectError)
+ self.clientGone(client)
+
+ d2 = factory.afterConnect
+ d2.addErrback(_goneClientAfterError, client)
+ return client
+
+ def _doneError(result):
+ self._pendingConnects -= 1
+ return result
+
+ d = factory.onConnect
+ d.addCallbacks(_doneOK, _doneError)
+
+ return d
+
+ def _performRequestOnClient(self, client, request, *args, **kwargs):
+ """
+ Perform the given request on the given client.
+
+ @param client: A L{PooledMemCacheProtocol} that will be used to perform
+ the given request.
+
+ @param command: A C{str} representing an attribute of
+ L{MemCacheProtocol}.
+ @parma args: Any positional arguments that should be passed to
+ C{command}.
+ @param kwargs: Any keyword arguments that should be passed to
+ C{command}.
+
+ @return: A L{Deferred} that fires with the result of the given command.
+ """
+
+ def _freeClientAfterRequest(result):
+ self.clientFree(client)
+ return result
+
+ def _goneClientAfterError(result):
+ self.clientGone(client)
+ return result
+
+ self.clientBusy(client)
+ d = client.submitRequest(request, closeAfter=False)
+ d.addCallbacks(_freeClientAfterRequest, _goneClientAfterError)
+ return d
+
+ @inlineCallbacks
+ def submitRequest(self, request, *args, **kwargs):
+ """
+ Select an available client and perform the given request on it.
+
+ @param command: A C{str} representing an attribute of
+ L{MemCacheProtocol}.
+ @parma args: Any positional arguments that should be passed to
+ C{command}.
+ @param kwargs: Any keyword arguments that should be passed to
+ C{command}.
+
+ @return: A L{Deferred} that fires with the result of the given command.
+ """
+
+ # Try this maxRetries times
+ for ctr in xrange(self.maxRetries + 1):
+ try:
+ response = (yield self._submitRequest(request, args, kwargs))
+ except (ConnectionLost, ConnectionDone, ConnectError), e:
+ self.log_error("HTTP pooled client connection error (attempt: %d) - retrying: %s" % (ctr+1, e,))
+ continue
+
+ # TODO: find the proper cause of these assertions and fix
+ except (AssertionError,), e:
+ self.log_error("HTTP pooled client connection assertion error (attempt: %d) - retrying: %s" % (ctr+1, e,))
+ continue
+ else:
+ returnValue(response)
+ else:
+ self.log_error("HTTP pooled client connection error - exhausted retry attempts.")
+ raise HTTPError(StatusResponse(responsecode.BAD_GATEWAY, "Could not connect to HTTP pooled client host."))
+
+ def _submitRequest(self, request, *args, **kwargs):
+ """
+ Select an available client and perform the given request on it.
+
+ @param command: A C{str} representing an attribute of
+ L{MemCacheProtocol}.
+ @parma args: Any positional arguments that should be passed to
+ C{command}.
+ @param kwargs: Any keyword arguments that should be passed to
+ C{command}.
+
+ @return: A L{Deferred} that fires with the result of the given command.
+ """
+
+ if len(self._freeClients) > 0:
+ d = self._performRequestOnClient(self._freeClients.pop(), request, *args, **kwargs)
+
+ elif len(self._busyClients) + self._pendingConnects >= self._maxClients:
+ d = Deferred()
+ self._pendingRequests.append((d, request, args, kwargs))
+ self.log_debug("Request queued: %s, %r, %r" % (request, args, kwargs))
+ self._logClientStats()
+
+ else:
+ d = self._newClientConnection()
+ d.addCallback(self._performRequestOnClient, request, *args, **kwargs)
+
+ return d
+
+ def _logClientStats(self):
+ self.log_debug("Clients #free: %d, #busy: %d, "
+ "#pending: %d, #queued: %d" % (
+ len(self._freeClients),
+ len(self._busyClients),
+ self._pendingConnects,
+ len(self._pendingRequests)))
+
+ def clientGone(self, client):
+ """
+ Notify that the given client is to be removed from the pool completely.
+
+ @param client: An instance of L{PooledMemCacheProtocol}.
+ """
+ if client in self._busyClients:
+ self._busyClients.remove(client)
+
+ elif client in self._freeClients:
+ self._freeClients.remove(client)
+
+ self.log_debug("Removed client: %r" % (client,))
+ self._logClientStats()
+
+ self._processPending()
+
+ def clientBusy(self, client):
+ """
+ Notify that the given client is being used to complete a request..
+
+ @param client: An instance of C{self.clientFactory}
+ """
+
+ if client in self._freeClients:
+ self._freeClients.remove(client)
+
+ self._busyClients.add(client)
+
+ self.log_debug("Busied client: %r" % (client,))
+ self._logClientStats()
+
+ def clientFree(self, client):
+ """
+ Notify that the given client is free to handle more requests.
+
+ @param client: An instance of C{self.clientFactory}
+ """
+ if client in self._busyClients:
+ self._busyClients.remove(client)
+
+ self._freeClients.add(client)
+
+ if self.shutdown_deferred and self._isIdle():
+ self.shutdown_deferred.callback(None)
+
+ self.log_debug("Freed client: %r" % (client,))
+ self._logClientStats()
+
+ self._processPending()
+
+ def _processPending(self):
+ if len(self._pendingRequests) > 0:
+ d, request, args, kwargs = self._pendingRequests.pop(0)
+
+ self.log_debug("Performing Queued Request: %s, %r, %r" % (
+ request, args, kwargs))
+ self._logClientStats()
+
+ _ign_d = self._submitRequest(request, *args, **kwargs)
+
+ _ign_d.addCallbacks(d.callback, d.errback)
+
+ def suggestMaxClients(self, maxClients):
+ """
+ Suggest the maximum number of concurrently connected clients.
+
+ @param maxClients: A C{int} indicating how many client connections we
+ should keep open.
+ """
+ self._maxClients = maxClients
+
+_clientPools = {} # Maps a host:port to a pool object
+
+def installPools(hosts, maxClients=5, reactor=None):
+
+ for name, url in hosts:
+ installPool(
+ name,
+ url,
+ maxClients,
+ reactor,
+ )
+
+def installPool(name, url, maxClients=5, reactor=None):
+
+ parsedURL = urlparse.urlparse(url)
+ pool = HTTPClientPool(
+ name,
+ parsedURL.scheme,
+ IPv4Address(
+ "TCP",
+ parsedURL.hostname,
+ parsedURL.port,
+ ),
+ maxClients,
+ reactor,
+ )
+ _clientPools[name] = pool
+
+def getHTTPClientPool(name):
+ return _clientPools[name]
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/reverseproxy.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,67 +0,0 @@
-##
-# Copyright (c) 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-__all__ = [
- "ReverseProxyResource",
-]
-
-from twisted.web2 import iweb
-from twisted.web2.client.http import ClientRequest
-from twisted.web2.resource import LeafResource
-
-from twistedcaldav.client.pool import getHTTPClientPool
-from twistedcaldav.log import LoggingMixIn
-
-import urllib
-from zope.interface.declarations import implements
-
-class ReverseProxyResource(LeafResource, LoggingMixIn):
- """
- A L{LeafResource} which always performs a reverse proxy operation.
- """
- implements(iweb.IResource)
-
- def __init__(self, poolID, *args, **kwargs):
- """
-
- @param poolID: idenitifier of the pool to use
- @type poolID: C{str}
- """
-
- self.poolID = poolID
- self._args = args
- self._kwargs = kwargs
-
- def isCollection(self):
- return True
-
- def exists(self):
- return False
-
- def renderHTTP(self, request):
- """
- Do the reverse proxy request and return the response.
-
- @param request: the incoming request that needs to be proxied.
- @type request: L{Request}
-
- @return: Deferred L{Response}
- """
-
- self.logger.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
- clientPool = getHTTPClientPool(self.poolID)
- proxyRequest = ClientRequest(request.method, request.uri, request.headers, request.stream)
- return clientPool.submitRequest(proxyRequest)
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/client/reverseproxy.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/client/reverseproxy.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,67 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+ "ReverseProxyResource",
+]
+
+from twisted.web2 import iweb
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.resource import LeafResource
+
+from twistedcaldav.client.pool import getHTTPClientPool
+from twistedcaldav.log import LoggingMixIn
+
+import urllib
+from zope.interface.declarations import implements
+
+class ReverseProxyResource(LeafResource, LoggingMixIn):
+ """
+ A L{LeafResource} which always performs a reverse proxy operation.
+ """
+ implements(iweb.IResource)
+
+ def __init__(self, poolID, *args, **kwargs):
+ """
+
+ @param poolID: idenitifier of the pool to use
+ @type poolID: C{str}
+ """
+
+ self.poolID = poolID
+ self._args = args
+ self._kwargs = kwargs
+
+ def isCollection(self):
+ return True
+
+ def exists(self):
+ return False
+
+ def renderHTTP(self, request):
+ """
+ Do the reverse proxy request and return the response.
+
+ @param request: the incoming request that needs to be proxied.
+ @type request: L{Request}
+
+ @return: Deferred L{Response}
+ """
+
+ self.logger.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
+ clientPool = getHTTPClientPool(self.poolID)
+ proxyRequest = ClientRequest(request.method, request.uri, request.headers, request.stream)
+ return clientPool.submitRequest(proxyRequest)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cluster.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cluster.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/cluster.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -120,10 +120,6 @@
'-o', 'MultiProcess/ProcessCount=%d' % (
config.MultiProcess['ProcessCount'],)])
- if config.Memcached["ServerEnabled"]:
- args.extend(
- ['-o', 'Memcached/ClientEnabled=True'])
-
if self.ports:
args.extend([
'-o',
@@ -469,22 +465,23 @@
config.PythonDirector['pydir'],
fname],
env=parentEnv)
- if config.Memcached["ServerEnabled"]:
- log.msg("Adding memcached service")
+ for name, pool in config.Memcached["Pools"].items():
+ if pool["ServerEnabled"]:
+ log.msg("Adding memcached service for pool: %s" % (name,))
+
+ memcachedArgv = [
+ config.Memcached["memcached"],
+ '-p', str(pool["Port"]),
+ '-l', pool["BindAddress"]]
+
+ if config.Memcached["MaxMemory"] is not 0:
+ memcachedArgv.extend([
+ '-m', str(config.Memcached["MaxMemory"])])
+
+ memcachedArgv.extend(config.Memcached["Options"])
+
+ monitor.addProcess('memcached-%s' % (name,), memcachedArgv, env=parentEnv)
- memcachedArgv = [
- config.Memcached["memcached"],
- '-p', str(config.Memcached["Port"]),
- '-l', config.Memcached["BindAddress"]]
-
- if config.Memcached["MaxMemory"] is not 0:
- memcachedArgv.extend([
- '-m', str(config.Memcached["MaxMemory"])])
-
- memcachedArgv.extend(config.Memcached["Options"])
-
- monitor.addProcess('memcached', memcachedArgv, env=parentEnv)
-
if (config.Notifications["Enabled"] and
config.Notifications["InternalNotificationHost"] == "localhost"):
log.msg("Adding notification service")
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/config.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/config.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -23,7 +23,6 @@
"config",
]
-import os
import copy
import re
@@ -33,6 +32,7 @@
from twistedcaldav.py.plistlib import readPlist
from twistedcaldav.log import Logger
from twistedcaldav.log import clearLogLevels, setLogLevelForNamespace, InvalidLogLevelError
+from twistedcaldav.partitions import partitions
log = Logger()
@@ -71,12 +71,37 @@
},
"twistedcaldav.directory.appleopendirectory.OpenDirectoryService": {
"node": "/Search",
- "requireComputerRecord": True,
"cacheTimeout": 30,
"signalIntervalSeconds": 10,
},
}
+augmentDefaultParams = {
+ "twistedcaldav.directory.augment.AugmentXMLDB": {
+ "xmlFiles": ["/etc/caldavd/augments.xml",],
+ },
+ "twistedcaldav.directory.augment.AugmentSqliteDB": {
+ "dbpath": "/etc/caldavd/augments.sqlite",
+ },
+ "twistedcaldav.directory.augment.AugmentPostgreSQLDB": {
+ "host": "localhost",
+ "database": "augments",
+ },
+}
+
+proxyDBDefaultParams = {
+ "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB": {
+ "dbpath": "/etc/caldavd/proxies.sqlite",
+ },
+ "twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB": {
+ "host": "localhost",
+ "database": "proxies",
+ "user": "",
+ "password": "",
+ },
+}
+
+
defaultConfig = {
# Note: Don't use None values below; that confuses the command-line parser.
@@ -125,6 +150,25 @@
},
#
+ # Augment service
+ #
+ # Augments for the directory service records to add calendar specific attributes.
+ #
+ "AugmentService": {
+ "type": "twistedcaldav.directory.augment.AugmentXMLDB",
+ "params": augmentDefaultParams["twistedcaldav.directory.augment.AugmentXMLDB"],
+ },
+
+ #
+ # Proxies
+ #
+ "ProxyDBService": {
+ "type": "twistedcaldav.directory.calendaruserproxy.ProxySqliteDB",
+ "params": proxyDBDefaultParams["twistedcaldav.directory.calendaruserproxy.ProxySqliteDB"],
+ },
+ "ProxyLoadFromFile": "", # Allows for initialization of the proxy database from an XML file
+
+ #
# Special principals
#
"AdminPrincipals": [], # Principals with "DAV:all" access (relative URLs)
@@ -224,6 +268,33 @@
"EnableAutoAcceptTrigger" : False, # Manually trigger auto-accept behavior
#
+ # Scheduling related options
+ #
+ "Scheduling": {
+
+ "CalDAV": {
+ "EmailDomain" : "", # Domain for mailto calendar user addresses on this server
+ "HTTPDomain" : "", # Domain for http calendar user addresses on this server
+ "AddressPatterns" : [], # Reg-ex patterns to match local calendar user addresses
+ "OldDraftCompatibility" : True, # Whether to maintain compatibility with non-implicit mode
+ "ScheduleTagCompatibility" : True, # Whether to support older clients that do not use Schedule-Tag feature
+ "EnablePrivateComments" : True, # Private comments from attendees to organizer
+ },
+
+ "iSchedule": {
+ "Enabled" : False, # iSchedule protocol
+ "AddressPatterns" : [], # Reg-ex patterns to match iSchedule-able calendar user addresses
+ "Servers" : "/etc/caldavd/servertoserver.xml", # iSchedule server configurations
+ },
+
+ "Options" : {
+ "AllowGroupAsOrganizer" : False, # Allow groups to be Organizers
+ "AllowLocationAsOrganizer" : False, # Allow locations to be Organizers
+ "AllowResourceAsOrganizer" : False, # Allow resources to be Organizers
+ }
+ },
+
+ #
# Notifications
#
"Notifications" : {
@@ -259,6 +330,16 @@
},
#
+ # Partitioning
+ #
+ "Partitioning" : {
+ "Enabled": False, # Partitioning enabled or not
+ "ServerPartitionID": "", # Unique ID for this server's partition instance.
+ "PartitionConfigFile": "/etc/caldavd/partitions.plist", # File path for partition information
+ "MaxClients": 5, # Pool size for connections to each partition
+ },
+
+ #
# Performance tuning
#
@@ -327,10 +408,26 @@
"Memcached": {
"MaxClients": 5,
- "ClientEnabled": True,
- "ServerEnabled": True,
- "BindAddress": "127.0.0.1",
- "Port": 11211,
+ "Pools": {
+ "Default": {
+ "ClientEnabled": True,
+ "ServerEnabled": True,
+ "BindAddress": "127.0.0.1",
+ "Port": 11211,
+ "HandleCacheTypes": [
+ "Default",
+ ]
+ },
+# "ProxyDB": {
+# "ClientEnabled": True,
+# "ServerEnabled": True,
+# "BindAddress": "127.0.0.1",
+# "Port": 11211,
+# "HandleCacheTypes": [
+# "ProxyDB", "PrincipalToken",
+# ]
+# },
+ },
"memcached": "memcached", # Find in PATH
"MaxMemory": 0, # Megabytes
"Options": [],
@@ -360,6 +457,7 @@
self.updateDropBox,
self.updateLogLevels,
self.updateNotifications,
+ self.updatePartitions,
]
def __str__(self):
@@ -417,6 +515,18 @@
if param not in serviceDefaultParams[self._data.DirectoryService.type]:
del self._data.DirectoryService.params[param]
+ if self._data.AugmentService.type in augmentDefaultParams:
+ for param in tuple(self._data.AugmentService.params):
+ if param not in augmentDefaultParams[self._data.AugmentService.type]:
+ log.warn("Parameter %s is not supported by service %s" % (param, self._data.AugmentService.type))
+ del self._data.AugmentService.params[param]
+
+ if self._data.ProxyDBService.type in proxyDBDefaultParams:
+ for param in tuple(self._data.ProxyDBService.params):
+ if param not in proxyDBDefaultParams[self._data.ProxyDBService.type]:
+ log.warn("Parameter %s is not supported by service %s" % (param, self._data.ProxyDBService.type))
+ del self._data.ProxyDBService.params[param]
+
@staticmethod
def updateACLs(self, items):
#
@@ -527,6 +637,21 @@
except InvalidLogLevelError, e:
raise ConfigurationError("Invalid log level: %s" % (e.level))
+ @staticmethod
+ def updatePartitions(self, items):
+ #
+ # Partitions
+ #
+
+ if "Partitioning" in items:
+ if items["Partitioning"]["Enabled"]:
+ partitions.setSelfPartition(items["Partitioning"]["ServerPartitionID"])
+ partitions.setMaxClients(items["Partitioning"]["MaxClients"])
+ partitions.readConfig(items["Partitioning"]["PartitionConfigFile"])
+ partitions.installReverseProxies()
+ else:
+ partitions.clear()
+
def updateDefaults(self, items):
_mergeData(self._defaults, items)
self.update(items)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/customxml.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/customxml.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -315,6 +315,13 @@
protected = True
hidden = True
+class IScheduleInbox (davxml.WebDAVEmptyElement):
+ """
+ Denotes the resourcetype of a iSchedule Inbox.
+ (CalDAV-s2s-xx, section x.x.x)
+ """
+ namespace = calendarserver_namespace
+ name = "ischedule-inbox"
@@ -327,3 +334,4 @@
davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
davxml.ResourceType.timezones = davxml.ResourceType(Timezones())
+davxml.ResourceType.ischeduleinbox = davxml.ResourceType(IScheduleInbox())
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/database.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/database.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/database.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/database.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,556 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.enterprise.adbapi import ConnectionPool
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python.threadpool import ThreadPool
+
+from twistedcaldav.config import ConfigurationError
+from twistedcaldav.log import Logger
+
+import thread
+
+try:
+ import pgdb
+except:
+ pgdb = None
+
+"""
+Generic ADAPI database access object.
+"""
+
+__all__ = [
+ "AbstractADBAPIDatabase",
+]
+
+log = Logger()
+
+class ConnectionClosingThreadPool(ThreadPool):
+ """
+ A ThreadPool that closes connections for each worker thread
+ """
+
+ def _worker(self, o):
+ log.debug("Starting ADBAPI thread: %s" % (thread.get_ident(),))
+ ThreadPool._worker(self, o)
+ self._closeConnection()
+
+ def _closeConnection(self):
+
+ tid = thread.get_ident()
+ log.debug("Closing ADBAPI thread: %s" % (tid,))
+
+ conn = self.pool.connections.get(tid)
+ self.pool._close(conn)
+ del self.pool.connections[tid]
+
+class AbstractADBAPIDatabase(object):
+ """
+ A generic SQL database.
+ """
+
+ def __init__(self, dbID, dbapiName, dbapiArgs, persistent, **kwargs):
+ """
+
+ @param persistent: C{True} if the data in the DB must be perserved during upgrades,
+ C{False} if the DB data can be re-created from an external source.
+ @type persistent: bool
+ """
+ self.dbID = dbID
+ self.dbapiName = dbapiName
+ self.dbapiArgs = dbapiArgs
+ self.dbapikwargs = kwargs
+
+ self.persistent = persistent
+
+ self.initialized = False
+
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.pool)
+
+ @inlineCallbacks
+ def open(self):
+ """
+ Access the underlying database.
+ @return: a db2 connection object for this index's underlying data store.
+ """
+ if not self.initialized:
+
+ self.pool = ConnectionPool(self.dbapiName, *self.dbapiArgs, **self.dbapikwargs)
+
+ # sqlite3 is not thread safe which means we have to close the sqlite3 connections in the same thread that
+ # opened them. We need a special thread pool class that has a thread worker function that does a close
+ # when a thread is closed.
+ if self.dbapiName == "sqlite3":
+ self.pool.threadpool.stop()
+ self.pool.threadpool = ConnectionClosingThreadPool(1, 1)
+ self.pool.threadpool.start()
+ self.pool.threadpool.pool = self.pool
+
+ #
+ # Set up the schema
+ #
+ # Create CALDAV table if needed
+
+ test = (yield self._test_schema_table())
+ if test:
+ version = (yield self._db_value_for_sql("select VALUE from CALDAV where KEY = 'SCHEMA_VERSION'"))
+ dbtype = (yield self._db_value_for_sql("select VALUE from CALDAV where KEY = 'TYPE'"))
+
+ if (version != self._db_version()) or (dbtype != self._db_type()):
+
+ if dbtype != self._db_type():
+ log.err("Database %s has different type (%s vs. %s)"
+ % (self.dbID, dbtype, self._db_type()))
+
+ # Delete this index and start over
+ yield self._db_remove()
+ yield self._db_init()
+
+ elif version != self._db_version():
+ log.err("Database %s has different schema (v.%s vs. v.%s)"
+ % (self.dbID, version, self._db_version()))
+
+ # Upgrade the DB
+ yield self._db_upgrade(version)
+
+ else:
+ yield self._db_init()
+ self.initialized = True
+
+ def close(self):
+
+ if self.initialized:
+ self.pool.close()
+ self.pool = None
+ self.initialized = False
+
+ @inlineCallbacks
+ def clean(self):
+
+ if not self.initialized:
+ yield self.open()
+
+ yield self._db_empty_data_tables()
+
+ @inlineCallbacks
+ def execute(self, sql, *query_params):
+
+ if not self.initialized:
+ yield self.open()
+
+ yield self._db_execute(sql, *query_params)
+
+ @inlineCallbacks
+ def executescript(self, script):
+
+ if not self.initialized:
+ yield self.open()
+
+ yield self._db_execute_script(script)
+
+ @inlineCallbacks
+ def query(self, sql, *query_params):
+
+ if not self.initialized:
+ yield self.open()
+
+ result = (yield self._db_all_values_for_sql(sql, *query_params))
+ returnValue(result)
+
+ @inlineCallbacks
+ def queryList(self, sql, *query_params):
+
+ if not self.initialized:
+ yield self.open()
+
+ result = (yield self._db_values_for_sql(sql, *query_params))
+ returnValue(result)
+
+ @inlineCallbacks
+ def queryOne(self, sql, *query_params):
+
+ if not self.initialized:
+ yield self.open()
+
+ result = (yield self._db_value_for_sql(sql, *query_params))
+ returnValue(result)
+
+ def _db_version(self):
+ """
+ @return: the schema version assigned to this DB.
+ """
+ raise NotImplementedError
+
+ def _db_type(self):
+ """
+ @return: the collection type assigned to this DB.
+ """
+ raise NotImplementedError
+
+ def _test_schema_table(self):
+ return self._test_table("CALDAV")
+
+ @inlineCallbacks
+ def _db_init(self):
+ """
+ Initialise the underlying database tables.
+ """
+ log.msg("Initializing database %s" % (self.dbID,))
+
+ # TODO we need an exclusive lock of some kind here to prevent a race condition
+ # in which multiple processes try to create the tables.
+
+
+ yield self._db_init_schema_table()
+ yield self._db_init_data_tables()
+ yield self._db_recreate()
+
+ @inlineCallbacks
+ def _db_init_schema_table(self):
+ """
+ Initialise the underlying database tables.
+ @param db_filename: the file name of the index database.
+ @param q: a database cursor to use.
+ """
+
+ #
+ # CALDAV table keeps track of our schema version and type
+ #
+ yield self._create_table("CALDAV", (
+ ("KEY", "text unique"),
+ ("VALUE", "text unique"),
+ ), True)
+
+ yield self._db_execute(
+ """
+ insert or ignore into CALDAV (KEY, VALUE)
+ values ('SCHEMA_VERSION', :1)
+ """, (self._db_version(),)
+ )
+ yield self._db_execute(
+ """
+ insert or ignore into CALDAV (KEY, VALUE)
+ values ('TYPE', :1)
+ """, (self._db_type(),)
+ )
+
+ def _db_init_data_tables(self):
+ """
+ Initialise the underlying database tables.
+ """
+ raise NotImplementedError
+
+ def _db_empty_data_tables(self):
+ """
+ Delete the database tables.
+ """
+
+ # Implementations can override this to re-create data
+ pass
+
+ def _db_recreate(self):
+ """
+ Recreate the database tables.
+ """
+
+ # Implementations can override this to re-create data
+ pass
+
+ @inlineCallbacks
+ def _db_upgrade(self, old_version):
+ """
+ Upgrade the database tables.
+ """
+
+ if self.persistent:
+ yield self._db_upgrade_data_tables(old_version)
+ yield self._db_upgrade_schema()
+ else:
+ # Non-persistent DB's by default can be removed and re-created. However, for simple
+ # DB upgrades they SHOULD override this method and handle those for better performance.
+ yield self._db_remove()
+ yield self._db_init()
+
+ def _db_upgrade_data_tables(self, old_version):
+ """
+ Upgrade the data from an older version of the DB.
+ """
+ # Persistent DB's MUST override this method and do a proper upgrade. Their data
+ # cannot be thrown away.
+ raise NotImplementedError("Persistent databases MUST support an upgrade method.")
+
+ @inlineCallbacks
+ def _db_upgrade_schema(self):
+ """
+ Upgrade the stored schema version to the current one.
+ """
+ yield self._db_execute("insert or replace into CALDAV (KEY, VALUE) values ('SCHEMA_VERSION', :1)", (self._db_version(),))
+
+ @inlineCallbacks
+ def _db_remove(self):
+ """
+ Remove all database information (all the tables)
+ """
+ yield self._db_remove_data_tables()
+ yield self._db_remove_schema()
+
+ def _db_remove_data_tables(self):
+ """
+ Remove all the data from an older version of the DB.
+ """
+ raise NotImplementedError("Each database must remove its own tables.")
+
+ @inlineCallbacks
+ def _db_remove_schema(self):
+ """
+ Remove the stored schema version table.
+ """
+ yield self._db_execute("drop table if exists CALDAV")
+
+ @inlineCallbacks
+ def _db_all_values_for_sql(self, sql, *query_params):
+ """
+ Execute an SQL query and obtain the resulting values.
+ @param sql: the SQL query to execute.
+ @param query_params: parameters to C{sql}.
+ @return: an interable of values in the first column of each row
+ resulting from executing C{sql} with C{query_params}.
+ @raise AssertionError: if the query yields multiple columns.
+ """
+
+ sql = self._prepare_statement(sql)
+ results = (yield self.pool.runQuery(sql, *query_params))
+ returnValue(tuple(results))
+
+ @inlineCallbacks
+ def _db_values_for_sql(self, sql, *query_params):
+ """
+ Execute an SQL query and obtain the resulting values.
+
+ @param sql: the SQL query to execute.
+ @param query_params: parameters to C{sql}.
+ @return: an interable of values in the first column of each row
+ resulting from executing C{sql} with C{query_params}.
+ @raise AssertionError: if the query yields multiple columns.
+ """
+
+ sql = self._prepare_statement(sql)
+ results = (yield self.pool.runQuery(sql, *query_params))
+ returnValue(tuple([row[0] for row in results]))
+
+ @inlineCallbacks
+ def _db_value_for_sql(self, sql, *query_params):
+ """
+ Execute an SQL query and obtain a single value.
+
+ @param sql: the SQL query to execute.
+ @param query_params: parameters to C{sql}.
+ @return: the value resulting from the executing C{sql} with
+ C{query_params}.
+ @raise AssertionError: if the query yields multiple rows or columns.
+ """
+ value = None
+ for row in (yield self._db_values_for_sql(sql, *query_params)):
+ assert value is None, "Multiple values in DB for %s %s" % (sql, query_params)
+ value = row
+ returnValue(value)
+
+ def _db_execute(self, sql, *query_params):
+ """
+ Execute an SQL operation that returns None.
+
+ @param sql: the SQL query to execute.
+ @param query_params: parameters to C{sql}.
+ @return: an iterable of tuples for each row resulting from executing
+ C{sql} with C{query_params}.
+ """
+
+ sql = self._prepare_statement(sql)
+ return self.pool.runOperation(sql, *query_params)
+
+ """
+ Since different databases support different types of columns and modifiers on those we need to
+ have an "abstract" way of specifying columns in our code and then map the abstract specifiers to
+ the underlying DB's allowed types.
+
+ Types we can use are:
+
+ integer
+ text
+ text(n)
+ date
+ serial
+
+ The " unique" modifier can be appended to any of those.
+ """
+ def _map_column_types(self, type):
+ raise NotImplementedError
+
+ def _create_table(self, name, columns, ifnotexists=False):
+ raise NotImplementedError
+
+ def _test_table(self, name):
+ raise NotImplementedError
+
+ def _prepare_statement(self, sql):
+ raise NotImplementedError
+
+class ADBAPISqliteMixin(object):
+
+ @classmethod
+ def _map_column_types(self, coltype):
+
+ result = ""
+ splits = coltype.split()
+ if splits[0] == "integer":
+ result = "integer"
+ elif splits[0] == "text":
+ result = "text"
+ elif splits[0].startswith("text("):
+ result = splits[0]
+ elif splits[0] == "date":
+ result = "date"
+ elif splits[0] == "serial":
+ result = "integer primary key autoincrement"
+
+ if len(splits) > 1 and splits[1] == "unique":
+ result += " unique"
+
+ return result
+
+ @inlineCallbacks
+ def _create_table(self, name, columns, ifnotexists=False):
+
+ colDefs = ["%s %s" % (colname, self._map_column_types(coltype)) for colname, coltype in columns]
+ statement = "create table %s%s (%s)" % (
+ "if not exists " if ifnotexists else "",
+ name,
+ ", ".join(colDefs),
+ )
+ yield self._db_execute(statement)
+
+ @inlineCallbacks
+ def _test_table(self, name):
+ result = (yield self._db_value_for_sql("""
+ select (1) from SQLITE_MASTER
+ where TYPE = 'table' and NAME = '%s'
+ """ % (name,)))
+ returnValue(result)
+
+ def _prepare_statement(self, sql):
+ # We are going to use the sqlite syntax of :1, :2 etc for our
+ # internal statements so we do not need to remap those
+ return sql
+
+if pgdb:
+
+ class ADBAPIPostgreSQLMixin(object):
+
+ @classmethod
+ def _map_column_types(self, coltype):
+
+ result = ""
+ splits = coltype.split()
+ if splits[0] == "integer":
+ result = "integer"
+ elif splits[0] == "text":
+ result = "text"
+ elif splits[0].startswith("text("):
+ result = "char" + splits[0][4:]
+ elif splits[0] == "date":
+ result = "date"
+ elif splits[0] == "serial":
+ result = "serial"
+
+ if len(splits) > 1 and splits[1] == "unique":
+ result += " unique"
+
+ return result
+
+ @inlineCallbacks
+ def _create_table(self, name, columns, ifnotexists=False):
+
+ colDefs = ["%s %s" % (colname, self._map_column_types(coltype)) for colname, coltype in columns]
+ statement = "create table %s (%s)" % (
+ name,
+ ", ".join(colDefs),
+ )
+
+ try:
+ yield self._db_execute(statement)
+ except pgdb.DatabaseError:
+
+ if not ifnotexists:
+ raise
+
+ result = (yield self._test_table(name))
+ if not result:
+ raise
+
+ @inlineCallbacks
+ def _test_table(self, name):
+ result = (yield self._db_value_for_sql("""
+ select * from pg_tables
+ where tablename = '%s'
+ """ % (name.lower(),)))
+ returnValue(result)
+
+ @inlineCallbacks
+ def _db_init_schema_table(self):
+ """
+ Initialise the underlying database tables.
+ @param db_filename: the file name of the index database.
+ @param q: a database cursor to use.
+ """
+
+ #
+ # CALDAV table keeps track of our schema version and type
+ #
+ try:
+ yield self._create_table("CALDAV", (
+ ("KEY", "text unique"),
+ ("VALUE", "text unique"),
+ ), True)
+
+ yield self._db_execute(
+ """
+ insert into CALDAV (KEY, VALUE)
+ values ('SCHEMA_VERSION', :1)
+ """, (self._db_version(),)
+ )
+ yield self._db_execute(
+ """
+ insert into CALDAV (KEY, VALUE)
+ values ('TYPE', :1)
+ """, (self._db_type(),)
+ )
+ except pgdb.DatabaseError:
+ pass
+
+ def _prepare_statement(self, sql):
+ # Convert :1, :2 etc format into %s
+ ctr = 1
+ while sql.find(":%d" % (ctr,)) != -1:
+ sql = sql.replace(":%d" % (ctr,), "%s")
+ ctr += 1
+ return sql
+
+else:
+ class ADBAPIPostgreSQLMixin(object):
+
+ def __init__(self):
+ raise ConfigurationError("PostgreSQL module not available.")
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/apache.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/apache.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/apache.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,197 +0,0 @@
-##
-# Copyright (c) 2006-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Apache UserFile/GroupFile compatible directory service implementation.
-"""
-
-__all__ = [
- "BasicDirectoryService",
- "DigestDirectoryService",
-]
-
-from crypt import crypt
-
-from twisted.python.filepath import FilePath
-from twisted.cred.credentials import UsernamePassword
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import UnknownRecordTypeError
-
-class AbstractDirectoryService(DirectoryService):
- """
- Abstract Apache-compatible implementation of L{IDirectoryService}.
- """
- def __repr__(self):
- return "<%s %r: %r %r>" % (self.__class__.__name__, self.realmName, self.userFile, self.groupFile)
-
- def __init__(self, realmName, userFile, groupFile=None):
- super(AbstractDirectoryService, self).__init__()
-
- if type(userFile) is str:
- userFile = FilePath(userFile)
- if type(groupFile) is str:
- groupFile = FilePath(groupFile)
-
- self.realmName = realmName
- self.userFile = userFile
- self.groupFile = groupFile
-
- def recordTypes(self):
- recordTypes = (DirectoryService.recordType_users,)
- if self.groupFile is not None:
- recordTypes += (DirectoryService.recordType_groups,)
- return recordTypes
-
- def listRecords(self, recordType):
- for entryShortName, entryData in self.entriesForRecordType(recordType):
- if recordType == DirectoryService.recordType_users:
- yield self.userRecordClass(
- service = self,
- recordType = recordType,
- shortName = entryShortName,
- cryptPassword = entryData,
- )
-
- elif recordType == DirectoryService.recordType_groups:
- yield GroupRecord(
- service = self,
- recordType = recordType,
- shortName = entryShortName,
- members = entryData,
- )
-
- else:
- # Subclass should cover the remaining record types
- raise AssertionError("Unknown record type: %r" % (recordType,))
-
- def recordWithShortName(self, recordType, shortName):
- for entryShortName, entryData in self.entriesForRecordType(recordType):
- if entryShortName == shortName:
- if recordType == DirectoryService.recordType_users:
- return self.userRecordClass(
- service = self,
- recordType = recordType,
- shortName = entryShortName,
- cryptPassword = entryData,
- )
-
- if recordType == DirectoryService.recordType_groups:
- return GroupRecord(
- service = self,
- recordType = recordType,
- shortName = entryShortName,
- members = entryData,
- )
-
- # Subclass should cover the remaining record types
- raise AssertionError("Unknown record type: %r" % (recordType,))
-
- return None
-
- def entriesForRecordType(self, recordType):
- if recordType == DirectoryService.recordType_users:
- recordFile = self.userFile
- elif recordType == DirectoryService.recordType_groups:
- recordFile = self.groupFile
- else:
- raise UnknownRecordTypeError("Unknown record type: %s" % (recordType,))
-
- if recordFile is None:
- return
-
- for entry in recordFile.open():
- if entry and entry[0] != "#":
- shortName, rest = entry.rstrip("\n").split(":", 1)
- yield shortName, rest
-
-class AbstractDirectoryRecord(DirectoryRecord):
- """
- Abstract Apache-compatible implementation of L{IDirectoryRecord}.
- """
- def __init__(self, service, recordType, shortName):
- super(AbstractDirectoryRecord, self).__init__(
- service = service,
- recordType = recordType,
- guid = None,
- shortName = shortName,
- fullName = None,
- calendarUserAddresses = set(),
- autoSchedule = False,
- )
-
-class AbstractUserRecord(AbstractDirectoryRecord):
- def __init__(self, service, recordType, shortName, cryptPassword=None):
- super(AbstractUserRecord, self).__init__(service, recordType, shortName)
-
- self._cryptPassword = cryptPassword
-
- def groups(self):
- for group in self.service.listRecords(DirectoryService.recordType_groups):
- for member in group.members():
- if member == self:
- yield group
- continue
-
-class BasicUserRecord(AbstractUserRecord):
- """
- Apache UserFile implementation of L{IDirectoryRecord}.
- """
- def verifyCredentials(self, credentials):
- if self._cryptPassword in ("", "*", "x"):
- return False
-
- if isinstance(credentials, UsernamePassword):
- return crypt(credentials.password, self._cryptPassword) == self._cryptPassword
-
- return super(BasicUserRecord, self).verifyCredentials(credentials)
-
-class BasicDirectoryService(AbstractDirectoryService):
- """
- Apache UserFile/GroupFile implementation of L{IDirectoryService}.
- """
- baseGUID = "DDF1E45C-CADE-4FCD-8AE6-B4B41D72B325"
- userRecordClass = BasicUserRecord
-
-class DigestUserRecord(AbstractUserRecord):
- """
- Apache DigestUserFile implementation of L{IDirectoryRecord}.
- """
- def verifyCredentials(self, credentials):
- raise NotImplementedError()
-
-class DigestDirectoryService(AbstractDirectoryService):
- """
- Apache DigestUserFile/GroupFile implementation of L{IDirectoryService}.
- """
- baseGUID = "0C719D1B-0A14-4074-8740-6D96A7D0C787"
- userRecordClass = DigestUserRecord
-
-class GroupRecord(AbstractDirectoryRecord):
- """
- Apache GroupFile implementation of L{IDirectoryRecord}.
- """
- def __init__(self, service, recordType, shortName, members=()):
- super(GroupRecord, self).__init__(service, recordType, shortName)
-
- if type(members) is str:
- members = tuple(m.strip() for m in members.split(","))
-
- self._members = members
-
- def members(self):
- for shortName in self._members:
- yield self.service.recordWithShortName(DirectoryService.recordType_users, shortName)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/appleopendirectory.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/appleopendirectory.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -23,19 +23,13 @@
"OpenDirectoryInitError",
]
-import sys
-import os
import signal
-from random import random
-from uuid import UUID
+import sys
-from xml.parsers.expat import ExpatError
-
import opendirectory
import dsattributes
import dsquery
import memcacheclient
-import memcacheclient
import cPickle as pickle
try:
@@ -52,11 +46,10 @@
from twisted.python.filepath import FilePath
from twistedcaldav.config import config
+from twistedcaldav.directory import augment
from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
-from plistlib import readPlistFromString, readPlist
-
serverPreferences = '/Library/Preferences/com.apple.servermgr_info.plist'
saclGroup = 'com.apple.access_calendar'
@@ -69,11 +62,9 @@
def __repr__(self):
return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
- def __init__(self, node="/Search", requireComputerRecord=True, dosetup=True, doreload=True, cacheTimeout=30, signalIntervalSeconds=10, **kwds):
+ def __init__(self, node="/Search", dosetup=True, doreload=True, cacheTimeout=30, signalIntervalSeconds=10, **kwds):
"""
@param node: an OpenDirectory node name to bind to.
- @param requireComputerRecord: C{True} if the directory schema is to be used to determine
- which calendar users are enabled.
@param dosetup: if C{True} then the directory records are initialized,
if C{False} they are not.
This should only be set to C{False} when doing unit tests.
@@ -87,9 +78,6 @@
self.realmName = node
self.directory = directory
self.node = node
- self.requireComputerRecord = requireComputerRecord
- self.computerRecords = {}
- self.servicetags = set()
self.cacheTimeout = cacheTimeout
self.signalIntervalSeconds = signalIntervalSeconds
self._records = {}
@@ -99,21 +87,6 @@
self.isWorkgroupServer = False
if dosetup:
- if self.requireComputerRecord:
- try:
- self._lookupVHostRecord()
- except Exception, e:
- self.log_error("Unable to locate virtual host record: %s" % (e,))
- raise
-
- if os.path.exists(serverPreferences):
- serverInfo = readPlist(serverPreferences)
-
- self.isWorkgroupServer = serverInfo.get('ServiceConfig', {}).get('IsWorkgroupServer', False)
-
- if self.isWorkgroupServer:
- self.log_info("Enabling Workgroup Server compatibility mode")
-
if doreload:
for recordType in self.recordTypes():
self.recordsForType(recordType)
@@ -282,221 +255,11 @@
def _getMemcacheClient(self, refresh=False):
if refresh or not hasattr(self, "memcacheClient"):
self.memcacheClient = memcacheclient.ClientFactory.getClient(['%s:%s' %
- (config.Memcached.BindAddress, config.Memcached.Port)],
+ (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)],
debug=0, pickleProtocol=2)
return self.memcacheClient
- def _lookupVHostRecord(self):
- """
- Get the OD service record for this host.
- """
-
- # The server must have been configured with a virtual hostname.
- vhostname = config.ServerHostName
- if not vhostname:
- raise OpenDirectoryInitError(
- "There is no virtual hostname configured for the server for use with Open Directory (node=%s)"
- % (self.realmName,)
- )
-
- # Find a record in /Computers with an apple-serviceinfo attribute value equal to the virtual hostname
- # and return some useful attributes.
- attrs = [
- dsattributes.kDS1AttrGeneratedUID,
- dsattributes.kDSNAttrRecordName,
- dsattributes.kDSNAttrMetaNodeLocation,
- "dsAttrTypeNative:apple-serviceinfo",
- ]
-
- self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r)" % (
- self.directory,
- dsquery.match(
- "dsAttrTypeNative:apple-serviceinfo",
- vhostname,
- dsattributes.eDSContains,
- ).generate(),
- True, # case insentive for hostnames
- dsattributes.kDSStdRecordTypeComputers,
- attrs
- ))
- records = opendirectory.queryRecordsWithAttributes_list(
- self.directory,
- dsquery.match(
- "dsAttrTypeNative:apple-serviceinfo",
- vhostname,
- dsattributes.eDSContains,
- ).generate(),
- True, # case insentive for hostnames
- dsattributes.kDSStdRecordTypeComputers,
- attrs
- )
- self._parseComputersRecords(records, vhostname)
-
- def _parseComputersRecords(self, records, vhostname):
- # Must have some results
- if len(records) == 0:
- raise OpenDirectoryInitError(
- "Open Directory (node=%s) has no /Computers records with a virtual hostname: %s"
- % (self.realmName, vhostname)
- )
-
- # Now find all appropriate records and determine the enabled (only) service tags for each.
- for recordname, record in records:
- self._parseServiceInfo(vhostname, recordname, record)
-
- # Log all the matching records
- for key, value in self.computerRecords.iteritems():
- _ignore_recordname, enabled, servicetag = value
- self.log_info("Matched Directory record: %s with ServicesLocator: %s, state: %s" % (
- key,
- servicetag,
- {True:"enabled", False:"disabled"}[enabled]
- ))
-
- # Log all the enabled service tags - or generate an error if there are none
- if self.servicetags:
- for tag in self.servicetags:
- self.log_info("Enabled ServicesLocator: %s" % (tag,))
- else:
- raise OpenDirectoryInitError(
- "Open Directory (node=%s) no /Computers records with an enabled and valid "
- "calendar service were found matching virtual hostname: %s"
- % (self.realmName, vhostname)
- )
-
- def _parseServiceInfo(self, vhostname, recordname, record):
-
- # Extract some useful attributes
- recordguid = record[dsattributes.kDS1AttrGeneratedUID]
- recordlocation = "%s/Computers/%s" % (record[dsattributes.kDSNAttrMetaNodeLocation], recordname)
-
- # First check for apple-serviceinfo attribute
- plist = record.get("dsAttrTypeNative:apple-serviceinfo", None)
- if not plist:
- return False
-
- # Parse the plist and look for our special entry
- plist = readPlistFromString(plist)
- vhosts = plist.get("com.apple.macosxserver.virtualhosts", None)
- if not vhosts:
- self.log_error(
- "Open Directory (node=%s) %s record does not have a "
- "com.apple.macosxserver.virtualhosts in its apple-serviceinfo attribute value"
- % (self.realmName, recordlocation)
- )
- return False
-
- # Iterate over each vhost and find one that is a calendar service
- hostguid = None
- for key, value in vhosts.iteritems():
- serviceTypes = value.get("serviceType", None)
- if serviceTypes:
- for type in serviceTypes:
- if type == "calendar":
- hostguid = key
- break
-
- if not hostguid:
- # We can get false positives from the query - we ignore those.
- return False
-
- # Get host name
- hostname = vhosts[hostguid].get("hostname", None)
- if not hostname:
- self.log_error(
- "Open Directory (node=%s) %s record does not have "
- "any host name in its apple-serviceinfo attribute value"
- % (self.realmName, recordlocation)
- )
- return False
- if hostname != vhostname:
- # We can get false positives from the query - we ignore those.
- return False
-
- # Get host details. At this point we only check that it is present. We actually
- # ignore the details themselves (scheme/port) as we use our own config for that.
- hostdetails = vhosts[hostguid].get("hostDetails", None)
- if not hostdetails:
- self.log_error(
- "Open Directory (node=%s) %s record does not have "
- "any host details in its apple-serviceinfo attribute value"
- % (self.realmName, recordlocation)
- )
- return False
-
- # Look at the service data
- serviceInfos = vhosts[hostguid].get("serviceInfo", None)
- if not serviceInfos or not serviceInfos.has_key("calendar"):
- self.log_error(
- "Open Directory (node=%s) %s record does not have a "
- "calendar service in its apple-serviceinfo attribute value"
- % (self.realmName, recordlocation)
- )
- return False
- serviceInfo = serviceInfos["calendar"]
-
- # Check that this service is enabled
- enabled = serviceInfo.get("enabled", True)
-
- # Create the string we will use to match users with accounts on this server
- servicetag = "%s:%s:calendar" % (recordguid, hostguid)
-
- self.computerRecords[recordlocation] = (recordname, enabled, servicetag)
-
- if enabled:
- self.servicetags.add(servicetag)
-
- return True
-
- def _calendarUserAddresses(self, recordType, recordName, record):
- """
- Extract specific attributes from the directory record for use as calendar user address.
-
- @param recordName: a C{str} containing the record name being operated on.
- @param record: a C{dict} containing the attributes retrieved from the directory.
- @return: a C{set} of C{str} for each expanded calendar user address.
- """
- # Now get the addresses
- result = set()
-
- # Add each email address as a mailto URI
- emails = record.get(dsattributes.kDSNAttrEMailAddress)
- if emails is not None:
- if isinstance(emails, str):
- emails = [emails]
- for email in emails:
- result.add("mailto:%s" % (email.lower(),))
-
- return result
-
- def _parseResourceInfo(self, plist, guid, recordType, shortname):
- """
- Parse OD ResourceInfo attribute and extract information that the server needs.
-
- @param plist: the plist that is the attribute value.
- @type plist: str
- @param guid: the directory GUID of the record being parsed.
- @type guid: str
- @param shortname: the record shortname of the record being parsed.
- @type shortname: str
- @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
- """
- try:
- plist = readPlistFromString(plist)
- wpframework = plist.get("com.apple.WhitePagesFramework", {})
- autoaccept = wpframework.get("AutoAcceptsInvitation", False)
- proxy = wpframework.get("CalendaringDelegate", None)
- read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
- except (ExpatError, AttributeError), e:
- self.log_error(
- "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
- (recordType, shortname, guid, e, plist,)
- )
- raise ValueError("Invalid ResourceInfo")
-
- return (autoaccept, proxy, read_only_proxy,)
-
def recordTypes(self):
return (
DirectoryService.recordType_users,
@@ -694,29 +457,16 @@
if recordType == DirectoryService.recordType_groups:
groupsForGUID = {}
- elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- proxiesForGUID = {}
- readOnlyProxiesForGUID = {}
- def allowForACLs():
- return recordType in (
- DirectoryService.recordType_users,
- DirectoryService.recordType_groups,
- )
+ 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 ()
- def disableForCalendaring(recordShortName):
- self.log_debug(
- "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
- % (recordType, recordShortName)
- )
-
- def invalidRecord(recordShortName):
- self.log_error(
- "Directory (incorrectly) returned a record with no applicable "
- "ServicesLocator attribute: (%s) %s"
- % (recordType, recordShortName)
- )
-
def disableRecord(record):
self.log_warn("Record disabled due to conflict (record name and GUID must match): %s" % (record,))
@@ -736,36 +486,11 @@
del cuaddrs[cuaddr]
for (recordShortName, value) in results:
- enabledForCalendaring = True
- if self.requireComputerRecord:
- servicesLocators = value.get(dsattributes.kDSNAttrServicesLocator)
-
- if servicesLocators:
- if type(servicesLocators) is str:
- servicesLocators = (servicesLocators,)
-
- for locator in servicesLocators:
- if locator in self.servicetags:
- break
- else:
- if allowForACLs():
- disableForCalendaring(recordShortName)
- enabledForCalendaring = False
- else:
- invalidRecord(recordShortName)
- continue
- else:
- if allowForACLs():
- disableForCalendaring(recordShortName)
- enabledForCalendaring = False
- else:
- invalidRecord(recordShortName)
- continue
-
# Now get useful record info.
recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
recordFullName = value.get(dsattributes.kDS1AttrDistinguishedName)
+ recordEmailAddresses = _setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
recordNodeName = value.get(dsattributes.kDSNAttrMetaNodeLocation)
if not recordGUID:
@@ -773,12 +498,6 @@
% (recordType, recordShortName, recordNodeName))
continue
- # Get calendar user addresses from directory record.
- if enabledForCalendaring:
- calendarUserAddresses = self._calendarUserAddresses(recordType, recordShortName, value)
- else:
- calendarUserAddresses = ()
-
# Special case for groups, which have members.
if recordType == DirectoryService.recordType_groups:
memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
@@ -794,22 +513,6 @@
else:
memberGUIDs = ()
- # Special case for resources and locations
- autoSchedule = False
- proxyGUIDs = ()
- readOnlyProxyGUIDs = ()
- if recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
- if resourceInfo is not None:
- try:
- autoSchedule, proxy, read_only_proxy = self._parseResourceInfo(resourceInfo, recordGUID, recordType, recordShortName)
- except ValueError:
- continue
- if proxy:
- proxyGUIDs = (proxy,)
- if read_only_proxy:
- readOnlyProxyGUIDs = (read_only_proxy,)
-
record = OpenDirectoryRecord(
service = self,
recordType = recordType,
@@ -817,14 +520,16 @@
nodeName = recordNodeName,
shortName = recordShortName,
fullName = recordFullName,
- calendarUserAddresses = calendarUserAddresses,
- autoSchedule = autoSchedule,
- enabledForCalendaring = enabledForCalendaring,
+ emailAddresses = recordEmailAddresses,
memberGUIDs = memberGUIDs,
- proxyGUIDs = proxyGUIDs,
- readOnlyProxyGUIDs = readOnlyProxyGUIDs,
)
+ # Look up augment information
+ # TODO: this needs to be deferred but for now we hard code the deferred result because
+ # we know it is completing immediately.
+ d = augment.AugmentService.getAugmentRecord(record.guid)
+ d.addCallback(lambda x:record.addAugmentInformation(x))
+
# Check for disabled items
if record.shortName in disabledNames or record.guid in disabledGUIDs:
disableRecord(record)
@@ -852,11 +557,6 @@
if recordType == DirectoryService.recordType_groups:
self._indexGroup(record, record._memberGUIDs, groupsForGUID)
- # Do proxy indexing if needed
- elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- self._indexGroup(record, record._proxyGUIDs, proxiesForGUID)
- self._indexGroup(record, record._readOnlyProxyGUIDs, readOnlyProxiesForGUID)
-
#
# Replace the entire cache
#
@@ -874,11 +574,6 @@
if recordType == DirectoryService.recordType_groups:
storage["groupsForGUID"] = groupsForGUID
- # Add proxy indexing if needed
- elif recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- storage["proxiesForGUID"] = proxiesForGUID
- storage["readOnlyProxiesForGUID"] = readOnlyProxiesForGUID
-
self._records[recordType] = storage
self.log_info(
@@ -895,7 +590,6 @@
dsattributes.kDSNAttrMetaNodeLocation,
]
- query = None
if recordType == DirectoryService.recordType_users:
listRecordType = dsattributes.kDSStdRecordTypeUsers
@@ -906,128 +600,24 @@
elif recordType == DirectoryService.recordType_locations:
listRecordType = dsattributes.kDSStdRecordTypePlaces
- attrs.append(dsattributes.kDSNAttrResourceInfo)
elif recordType == DirectoryService.recordType_resources:
listRecordType = dsattributes.kDSStdRecordTypeResources
- attrs.append(dsattributes.kDSNAttrResourceInfo)
else:
raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
- if self.requireComputerRecord:
- if self.isWorkgroupServer and recordType == DirectoryService.recordType_users:
- self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
- self.directory,
- dsattributes.kDSNAttrRecordName,
- saclGroup,
- dsattributes.eDSExact,
- False,
- dsattributes.kDSStdRecordTypeGroups,
- [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups],
- ))
- results = opendirectory.queryRecordsWithAttribute_list(
- self.directory,
- dsattributes.kDSNAttrRecordName,
- saclGroup,
- dsattributes.eDSExact,
- False,
- dsattributes.kDSStdRecordTypeGroups,
- [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
- )
-
- if len(results) == 1:
- members = results[0][1].get(dsattributes.kDSNAttrGroupMembers, [])
- nestedGroups = results[0][1].get(dsattributes.kDSNAttrNestedGroups, [])
- else:
- members = []
- nestedGroups = []
-
- guidQueries = []
-
- for GUID in self._expandGroupMembership(members, nestedGroups):
- guidQueries.append(
- dsquery.match(dsattributes.kDS1AttrGeneratedUID, GUID, dsattributes.eDSExact)
- )
-
- if not guidQueries:
- self.log_warn("No SACL enabled users found.")
- return ()
-
- query = dsquery.expression(dsquery.expression.OR, guidQueries)
-
- #
- # For users and groups, we'll load all entries, even if
- # they don't have a services locator for this server.
- #
- elif (
- recordType != DirectoryService.recordType_users and
- recordType != DirectoryService.recordType_groups
- ):
- tag_queries = []
-
- for tag in self.servicetags:
- tag_queries.append(dsquery.match(dsattributes.kDSNAttrServicesLocator, tag, dsattributes.eDSExact))
-
- if len(tag_queries) == 1:
- subquery = tag_queries[0]
- else:
- subquery = dsquery.expression(dsquery.expression.OR, tag_queries)
-
- if query is None:
- query = subquery
- else:
- query = dsquery.expression(dsquery.expression.AND, (subquery, query))
-
- subquery = None
-
try:
- if query:
- if isinstance(query, dsquery.match):
- self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
- self.directory,
- query.attribute,
- query.value,
- query.matchType,
- False,
- listRecordType,
- attrs,
- ))
- results = opendirectory.queryRecordsWithAttribute_list(
- self.directory,
- query.attribute,
- query.value,
- query.matchType,
- False,
- listRecordType,
- attrs,
- )
- else:
- self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r)" % (
- self.directory,
- query.generate(),
- False,
- listRecordType,
- attrs,
- ))
- results = opendirectory.queryRecordsWithAttributes_list(
- self.directory,
- query.generate(),
- False,
- listRecordType,
- attrs,
- )
- else:
- self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
- self.directory,
- listRecordType,
- attrs,
- ))
- results = opendirectory.listAllRecordsWithAttributes_list(
- self.directory,
- listRecordType,
- attrs,
- )
+ self.log_debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
+ self.directory,
+ listRecordType,
+ attrs,
+ ))
+ results = opendirectory.listAllRecordsWithAttributes_list(
+ self.directory,
+ listRecordType,
+ attrs,
+ )
except opendirectory.ODError, ex:
self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
raise
@@ -1040,8 +630,7 @@
"""
def __init__(
self, service, recordType, guid, nodeName, shortName, fullName,
- calendarUserAddresses, autoSchedule, enabledForCalendaring,
- memberGUIDs, proxyGUIDs, readOnlyProxyGUIDs,
+ emailAddresses, memberGUIDs,
):
super(OpenDirectoryRecord, self).__init__(
service = service,
@@ -1049,14 +638,10 @@
guid = guid,
shortName = shortName,
fullName = fullName,
- calendarUserAddresses = calendarUserAddresses,
- autoSchedule = autoSchedule,
- enabledForCalendaring = enabledForCalendaring,
+ emailAddresses = emailAddresses,
)
self.nodeName = nodeName
self._memberGUIDs = tuple(memberGUIDs)
- self._proxyGUIDs = tuple(proxyGUIDs)
- self._readOnlyProxyGUIDs = tuple(readOnlyProxyGUIDs)
def __repr__(self):
if self.service.realmName == self.nodeName:
@@ -1086,41 +671,6 @@
def groups(self):
return self.service.groupsForGUID(self.guid)
- def proxies(self):
- if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- return
-
- for guid in self._proxyGUIDs:
- proxyRecord = self.service.recordWithGUID(guid)
- if proxyRecord is None:
- self.log_error("No record for proxy in %s with GUID %s" % (self.shortName, guid))
- else:
- yield proxyRecord
-
- def proxyFor(self):
- result = set()
- result.update(self.service.proxiesForGUID(DirectoryService.recordType_resources, self.guid))
- result.update(self.service.proxiesForGUID(DirectoryService.recordType_locations, self.guid))
- return result
-
- def readOnlyProxies(self):
- if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- return
-
- for guid in self._readOnlyProxyGUIDs:
- proxyRecord = self.service.recordWithGUID(guid)
- if proxyRecord is None:
- self.log_error("No record for proxy in %s with GUID %s" % (self.shortName, guid))
- else:
- yield proxyRecord
-
- def readOnlyProxyFor(self):
- result = set()
- result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_resources, self.guid))
- result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_locations, self.guid))
- return result
-
-
def getMemcacheKey(self, shortName):
key = "auth-%s" % (md5(shortName).hexdigest(),)
return key
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/augment.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/augment.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/augment.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/augment.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,364 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
+ ADBAPIPostgreSQLMixin
+from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
+import copy
+import time
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+class AugmentRecord(object):
+ """
+ Augmented directory record information
+ """
+
+ def __init__(
+ self,
+ guid,
+ enabled=False,
+ hostedAt="",
+ enabledForCalendaring=False,
+ autoSchedule=False,
+ calendarUserAddresses=None,
+ ):
+ self.guid = guid
+ self.enabled = enabled
+ self.hostedAt = hostedAt
+ self.enabledForCalendaring = enabledForCalendaring
+ self.autoSchedule = autoSchedule
+ self.calendarUserAddresses = calendarUserAddresses if calendarUserAddresses else set()
+
+class AugmentDB(object):
+ """
+ Abstract base class for an augment record database.
+ """
+
+ def __init__(self):
+ pass
+
+ @inlineCallbacks
+ def getAugmentRecord(self, guid):
+ """
+ Get an AugmentRecord for the specified GUID or the default.
+
+ @param guid: directory GUID to lookup
+ @type guid: C{str}
+
+ @return: L{Deferred}
+ """
+
+ result = (yield self._lookupAugmentRecord(guid))
+ if result is None:
+ if not hasattr(self, "_defaultRecord"):
+ self._defaultRecord = (yield self._lookupAugmentRecord("Default"))
+ if self._defaultRecord is not None:
+ result = copy.deepcopy(self._defaultRecord)
+ result.guid = guid
+ returnValue(result)
+
+ @inlineCallbacks
+ def getAllGUIDs(self):
+ """
+ Get all AugmentRecord GUIDs.
+
+ @return: L{Deferred}
+ """
+
+ raise NotImplementedError("Child class must define this.")
+
+ def _lookupAugmentRecord(self, guid):
+ """
+ Get an AugmentRecord for the specified GUID.
+
+ @param guid: directory GUID to lookup
+ @type guid: C{str}
+
+ @return: L{Deferred}
+ """
+
+ raise NotImplementedError("Child class must define this.")
+
+ def refresh(self):
+ """
+ Refresh any cached data.
+ """
+ pass
+
+AugmentService = AugmentDB() # Global augment service
+
+
+class AugmentXMLDB(AugmentDB):
+ """
+ XMLFile based augment database implementation.
+ """
+
+ def __init__(self, xmlFiles, cacheTimeout=30):
+
+ self.xmlFiles = xmlFiles
+ self.cacheTimeout = cacheTimeout * 60 # Value is mins we want secs
+ self.lastCached = 0
+ self.db = {}
+
+ try:
+ self.db = self._parseXML()
+ except RuntimeError:
+ log.error("Failed to parse XML augments file - fatal error on startup")
+ raise
+
+ self.lastCached = time.time()
+
+ @inlineCallbacks
+ def getAllGUIDs(self):
+ """
+ Get all AugmentRecord GUIDs.
+
+ @return: L{Deferred}
+ """
+
+ return succeed(self.db.keys())
+
+ def _lookupAugmentRecord(self, guid):
+ """
+ Get an AugmentRecord for the specified GUID.
+
+ @param guid: directory GUID to lookup
+ @type guid: C{str}
+
+ @return: L{Deferred}
+ """
+
+ # May need to re-cache
+ if self.lastCached + self.cacheTimeout <= time.time():
+ self.refresh()
+
+ return succeed(self.db.get(guid))
+
+ def refresh(self):
+ """
+ Refresh any cached data.
+ """
+ try:
+ self.db = self._parseXML()
+ except RuntimeError:
+ log.error("Failed to parse XML augments file during cache refresh - ignoring")
+ self.lastCached = time.time()
+
+ def _parseXML(self):
+
+ # Do each file
+ results = {}
+ for xmlFile in self.xmlFiles:
+
+ # Creating a parser does the parse
+ XMLAugmentsParser(xmlFile, results)
+
+ return results
+
+class AugmentADAPI(AugmentDB, AbstractADBAPIDatabase):
+ """
+ DBAPI based augment database implementation.
+ """
+
+ schema_version = "1"
+ schema_type = "AugmentDB"
+
+ def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
+
+ self.cachedPartitions = {}
+ self.cachedHostedAt = {}
+
+ AbstractADBAPIDatabase.__init__(self, dbID, dbapiName, dbapiArgs, True, **kwargs)
+
+ @inlineCallbacks
+ def getAllGUIDs(self):
+ """
+ Get all AugmentRecord GUIDs.
+
+ @return: L{Deferred}
+ """
+
+ # Query for the record information
+ results = (yield self.queryList("select GUID from AUGMENTS", ()))
+ returnValue(results)
+
+ @inlineCallbacks
+ def _lookupAugmentRecord(self, guid):
+ """
+ Get an AugmentRecord for the specified GUID.
+
+ @param guid: directory GUID to lookup
+ @type guid: C{str}
+
+ @return: L{Deferred}
+ """
+
+ # Query for the record information
+ results = (yield self.query("select GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE, CUADDRS from AUGMENTS where GUID = :1", (guid,)))
+ if not results:
+ returnValue(None)
+ else:
+ guid, enabled, partitionid, enabdledForCalendaring, autoSchedule, cuaddrs = results[0]
+
+ record = AugmentRecord(
+ guid = guid,
+ enabled = enabled == "T",
+ hostedAt = (yield self._getPartition(partitionid)),
+ enabledForCalendaring = enabdledForCalendaring == "T",
+ autoSchedule = autoSchedule == "T",
+ calendarUserAddresses = set(cuaddrs.split("\t")) if cuaddrs else set(),
+ )
+
+ returnValue(record)
+
+ @inlineCallbacks
+ def addAugmentRecord(self, record, update=False):
+
+ partitionid = (yield self._getPartitionID(record.hostedAt))
+ cuaddrs = "\t".join(record.calendarUserAddresses)
+
+ if update:
+ yield self.execute(
+ """update AUGMENTS set
+ (GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE, CUADDRS) =
+ (:1, :2, :3, :4, :5, :6) where GUID = :7""",
+ (
+ record.guid,
+ "T" if record.enabled else "F",
+ partitionid,
+ "T" if record.enabledForCalendaring else "F",
+ "T" if record.autoSchedule else "F",
+ cuaddrs,
+ record.guid,
+ )
+ )
+ else:
+ yield self.execute(
+ """insert into AUGMENTS
+ (GUID, ENABLED, PARTITIONID, CALENDARING, AUTOSCHEDULE, CUADDRS)
+ values (:1, :2, :3, :4, :5, :6)""",
+ (
+ record.guid,
+ "T" if record.enabled else "F",
+ partitionid,
+ "T" if record.enabledForCalendaring else "F",
+ "T" if record.autoSchedule else "F",
+ cuaddrs,)
+ )
+
+ def removeAugmentRecord(self, guid):
+
+ return self.query("delete from AUGMENTS where GUID = :1", (guid,))
+
+ @inlineCallbacks
+ def _getPartitionID(self, hostedat, createIfMissing=True):
+
+ # We will use a cache for these as we do not expect changes whilst running
+ try:
+ returnValue(self.cachedHostedAt[hostedat])
+ except KeyError:
+ pass
+
+ partitionid = (yield self.queryOne("select PARTITIONID from PARTITIONS where HOSTEDAT = :1", (hostedat,)))
+ if partitionid == None:
+ yield self.execute("insert into PARTITIONS (HOSTEDAT) values (:1)", (hostedat,))
+ partitionid = (yield self.queryOne("select PARTITIONID from PARTITIONS where HOSTEDAT = :1", (hostedat,)))
+ self.cachedHostedAt[hostedat] = partitionid
+ returnValue(partitionid)
+
+ @inlineCallbacks
+ def _getPartition(self, partitionid):
+
+ # We will use a cache for these as we do not expect changes whilst running
+ try:
+ returnValue(self.cachedPartitions[partitionid])
+ except KeyError:
+ pass
+
+ partition = (yield self.queryOne("select HOSTEDAT from PARTITIONS where PARTITIONID = :1", (partitionid,)))
+ self.cachedPartitions[partitionid] = partition
+ returnValue(partition)
+
+ @inlineCallbacks
+ def _getCUAddrs(self, augmentid):
+
+ return self.queryList("select CUADDR from CUADDRS where AUGMENTID = :1", (augmentid,))
+
+ def _db_version(self):
+ """
+ @return: the schema version assigned to this index.
+ """
+ return AugmentADAPI.schema_version
+
+ def _db_type(self):
+ """
+ @return: the collection type assigned to this index.
+ """
+ return AugmentADAPI.schema_type
+
+ @inlineCallbacks
+ def _db_init_data_tables(self):
+ """
+ Initialize the underlying database tables.
+ """
+
+ #
+ # TESTTYPE table
+ #
+ yield self._create_table("AUGMENTS", (
+ ("GUID", "text unique"),
+ ("ENABLED", "text(1)"),
+ ("PARTITIONID", "text"),
+ ("CALENDARING", "text(1)"),
+ ("AUTOSCHEDULE", "text(1)"),
+ ("CUADDRS", "text"),
+ ))
+
+ yield self._create_table("PARTITIONS", (
+ ("PARTITIONID", "serial"),
+ ("HOSTEDAT", "text"),
+ ))
+
+ @inlineCallbacks
+ def _db_empty_data_tables(self):
+ yield self._db_execute("delete from AUGMENTS")
+ yield self._db_execute("delete from PARTITIONS")
+
+class AugmentSqliteDB(ADBAPISqliteMixin, AugmentADAPI):
+ """
+ Sqlite based augment database implementation.
+ """
+
+ def __init__(self, dbpath):
+
+ ADBAPISqliteMixin.__init__(self)
+ AugmentADAPI.__init__(self, "Augments", "sqlite3", (dbpath,))
+
+class AugmentPostgreSQLDB(ADBAPIPostgreSQLMixin, AugmentADAPI):
+ """
+ PostgreSQL based augment database implementation.
+ """
+
+ def __init__(self, host, database, user=None, password=None):
+
+ ADBAPIPostgreSQLMixin.__init__(self)
+ AugmentADAPI.__init__(self, "Augments", "pgdb", (), host=host, database=database, user=user, password=password,)
+
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxy.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxy.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -20,6 +20,10 @@
__all__ = [
"CalendarUserProxyPrincipalResource",
+ "ProxyDB",
+ "ProxyDBService",
+ "ProxySqliteDB",
+ "ProxyPostgreSQLDB",
]
from twisted.internet.defer import returnValue
@@ -32,15 +36,17 @@
from twisted.web2.dav.noneprops import NonePropertyStore
from twistedcaldav.config import config
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin,\
+ ADBAPIPostgreSQLMixin
from twistedcaldav.extensions import DAVFile, DAVPrincipalResource
from twistedcaldav.extensions import ReadOnlyWritePropertiesResourceMixIn
from twistedcaldav.memcacher import Memcacher
from twistedcaldav.resource import CalDAVComplianceMixIn
from twistedcaldav.directory.util import NotFilePath
-from twistedcaldav.sql import AbstractSQLDatabase
+from twistedcaldav.log import LoggingMixIn
import itertools
-import os
+import time
class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
def defaultAccessControlList(self):
@@ -59,17 +65,19 @@
)
# Add admins
- aces += tuple([davxml.ACE(
- davxml.Principal(davxml.HRef(principal)),
- davxml.Grant(davxml.Privilege(davxml.All())),
- davxml.Protected(),
- ) for principal in config.AdminPrincipals
- ])
+ aces += tuple((
+ davxml.ACE(
+ davxml.Principal(davxml.HRef(principal)),
+ davxml.Grant(davxml.Privilege(davxml.All())),
+ davxml.Protected(),
+ )
+ for principal in config.AdminPrincipals
+ ))
return davxml.ACL(*aces)
def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
- # Permissions here are fixed, and are not subject to inherritance rules, etc.
+ # Permissions here are fixed, and are not subject to inheritance rules, etc.
return succeed(self.defaultAccessControlList())
class CalendarUserProxyPrincipalResource (CalDAVComplianceMixIn, PermissionsMixIn, DAVPrincipalResource, DAVFile):
@@ -116,14 +124,10 @@
"""
Return the SQL database for this group principal.
- @return: the L{CalendarUserProxyDatabase} for the principal collection.
+ @return: the L{ProxyDB} for the principal collection.
"""
+ return ProxyDBService
- # The db is located in the principal collection root
- if not hasattr(self.pcollection, "calendar_user_proxy_db"):
- setattr(self.pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(config.DataRoot))
- return self.pcollection.calendar_user_proxy_db
-
def resourceType(self):
if self.proxyType == "calendar-proxy-read":
return davxml.ResourceType.calendarproxyread
@@ -156,15 +160,7 @@
assert isinstance(property, davxml.WebDAVElement)
if property.qname() == (dav_namespace, "group-member-set"):
- if self.hasEditableMembership():
- return self.setGroupMemberSet(property, request)
- else:
- raise HTTPError(
- StatusResponse(
- responsecode.FORBIDDEN,
- "Proxies cannot be changed."
- )
- )
+ return self.setGroupMemberSet(property, request)
return super(CalendarUserProxyPrincipalResource, self).writeProperty(property, request)
@@ -182,8 +178,9 @@
# Break out the list into a set of URIs.
members = [str(h) for h in new_members.children]
- # Map the URIs to principals.
+ # Map the URIs to principals and a set of UIDs.
principals = []
+ newUIDs = set()
for uri in members:
principal = self.pcollection._principalForURI(uri)
# Invalid principals MUST result in an error.
@@ -193,14 +190,31 @@
"Attempt to use a non-existent principal %s as a group member of %s." % (uri, self.principalURL(),)
))
principals.append(principal)
- yield principal.cacheNotifier.changed()
+ newUIDs.add(principal.principalUID())
+ # Get the old set of UIDs
+ oldUIDs = (yield self._index().getMembers(self.uid))
+
+ # Change membership
+ yield self.setGroupMemberSetPrincipals(principals)
+
+ # Invalidate the primary principal's cache, and any principal's whose
+ # membership status changed
+ yield self.parent.cacheNotifier.changed()
+
+ changedUIDs = newUIDs.symmetric_difference(oldUIDs)
+ for uid in changedUIDs:
+ principal = self.pcollection.principalForUID(uid)
+ if principal:
+ yield principal.cacheNotifier.changed()
+
+ returnValue(True)
+
+ @inlineCallbacks
+ def setGroupMemberSetPrincipals(self, principals):
# Map the principals to UIDs.
uids = [p.principalUID() for p in principals]
-
yield self._index().setGroupMembers(self.uid, uids)
- yield self.parent.cacheNotifier.changed()
- returnValue(True)
##
# HTTP
@@ -246,8 +260,7 @@
"""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 (%s):\n""" % ({False:"Locked", True:"Editable"}[self.hasEditableMembership()])
- , format_principals(closure["members"]),
+ """\nGroup members:\n""" , format_principals(closure["members"]),
"""\nGroup memberships:\n""" , format_principals(closure["memberships"]),
"""</pre></blockquote></div>""",
closure["output"]
@@ -307,17 +320,28 @@
@inlineCallbacks
def _directGroupMembers(self):
- if self.hasEditableMembership():
- # Get member UIDs from database and map to principal resources
- members = yield self._index().getMembers(self.uid)
- returnValue([p for p in [self.pcollection.principalForUID(uid) for uid in members] if p])
- else:
- # Fixed proxies
- if self.proxyType == "calendar-proxy-write":
- returnValue(self.parent.proxies())
+ # Get member UIDs from database and map to principal resources
+ members = yield self._index().getMembers(self.uid)
+ found = []
+ missing = []
+ for uid in members:
+ p = self.pcollection.principalForUID(uid)
+ if p:
+ found.append(p)
+ # Make sure any outstanding deletion timer entries for
+ # existing principals are removed
+ yield self._index().refreshPrincipal(uid)
else:
- returnValue(self.parent.readOnlyProxies())
+ missing.append(uid)
+ # Clean-up ones that are missing
+ for uid in missing:
+ cacheTimeout = config.DirectoryService.params.get("cacheTimeout", 30) * 60 # in seconds
+
+ yield self._index().removePrincipal(uid, delay=cacheTimeout*2)
+
+ returnValue(found)
+
def groupMembers(self):
return self._expandMemberUIDs()
@@ -327,10 +351,7 @@
memberships = yield self._index().getMemberships(self.uid)
returnValue([p for p in [self.pcollection.principalForUID(uid) for uid in memberships] if p])
- def hasEditableMembership(self):
- return self.parent.hasEditableProxyMembership()
-
-class CalendarUserProxyDatabase(AbstractSQLDatabase):
+class ProxyDB(AbstractADBAPIDatabase, LoggingMixIn):
"""
A database to maintain calendar user proxy group memberships.
@@ -342,17 +363,16 @@
"""
- dbType = "CALENDARUSERPROXY"
- dbFilename = "calendaruserproxy.sqlite"
- dbFormatVersion = "4"
-
+ schema_version = "4"
+ schema_type = "ProxyDB"
+
class ProxyDBMemcacher(Memcacher):
def setMembers(self, guid, members):
- return self.set("members:%s" % (guid,), str(",".join(members)))
+ return self.set("members:%s" % (str(guid),), str(",".join(members)))
def setMemberships(self, guid, memberships):
- return self.set("memberships:%s" % (guid,), str(",".join(memberships)))
+ return self.set("memberships:%s" % (str(guid),), str(",".join(memberships)))
def getMembers(self, guid):
def _value(value):
@@ -362,7 +382,7 @@
return None
else:
return set()
- d = self.get("members:%s" % (guid,))
+ d = self.get("members:%s" % (str(guid),))
d.addCallback(_value)
return d
@@ -374,21 +394,47 @@
return None
else:
return set()
- d = self.get("memberships:%s" % (guid,))
+ d = self.get("memberships:%s" % (str(guid),))
d.addCallback(_value)
return d
def deleteMember(self, guid):
- return self.delete("members:%s" % (guid,))
+ return self.delete("members:%s" % (str(guid),))
def deleteMembership(self, guid):
- return self.delete("memberships:%s" % (guid,))
+ return self.delete("memberships:%s" % (str(guid),))
- def __init__(self, path):
- path = os.path.join(path, CalendarUserProxyDatabase.dbFilename)
- super(CalendarUserProxyDatabase, self).__init__(path, True)
+ def setDeletionTimer(self, guid, delay):
+ return self.set("del:%s" % (str(guid),), str(self.getTime()+delay))
+
+ def checkDeletionTimer(self, guid):
+ # True means it's overdue, False means it's not, None means no timer
+ def _value(value):
+ if value:
+ if int(value) <= self.getTime():
+ return True
+ else:
+ return False
+ else:
+ return None
+ d = self.get("del:%s" % (str(guid),))
+ d.addCallback(_value)
+ return d
+
+ def clearDeletionTimer(self, guid):
+ return self.delete("del:%s" % (str(guid),))
+
+ def getTime(self):
+ if hasattr(self, 'theTime'):
+ theTime = self.theTime
+ else:
+ theTime = int(time.time())
+ return theTime
+
+ def __init__(self, dbID, dbapiName, dbapiArgs, **kwargs):
+ AbstractADBAPIDatabase.__init__(self, dbID, dbapiName, dbapiArgs, True, **kwargs)
- self._memcacher = CalendarUserProxyDatabase.ProxyDBMemcacher("proxyDB")
+ self._memcacher = ProxyDB.ProxyDBMemcacher("ProxyDB")
@inlineCallbacks
def setGroupMembers(self, principalUID, members):
@@ -405,21 +451,46 @@
current_members = ()
current_members = set(current_members)
- # Remove what is there, then add it back.
- self._delete_from_db(principalUID)
- self._add_to_db(principalUID, members)
- self._db_commit()
-
- # Update cache
+ # Find changes
update_members = set(members)
-
remove_members = current_members.difference(update_members)
add_members = update_members.difference(current_members)
+
+ yield self.changeGroupMembersInDatabase(principalUID, add_members, remove_members)
+
+ # Update cache
for member in itertools.chain(remove_members, add_members,):
_ignore = yield self._memcacher.deleteMembership(member)
_ignore = yield self._memcacher.deleteMember(principalUID)
@inlineCallbacks
+ def setGroupMembersInDatabase(self, principalUID, members):
+ """
+ A blocking call to add a group membership record in the database.
+
+ @param principalUID: the UID of the group principal to add.
+ @param members: a list UIDs of principals that are members of this group.
+ """
+ # Remove what is there, then add it back.
+ yield self._delete_from_db(principalUID)
+ yield self._add_to_db(principalUID, members)
+
+ @inlineCallbacks
+ def changeGroupMembersInDatabase(self, principalUID, addMembers, removeMembers):
+ """
+ A blocking call to add a group membership record in the database.
+
+ @param principalUID: the UID of the group principal to add.
+ @param addMembers: a list UIDs of principals to be added as members of this group.
+ @param removeMembers: a list UIDs of principals to be removed as members of this group.
+ """
+ # Remove what is there, then add it back.
+ for member in removeMembers:
+ yield self._delete_from_db_one(principalUID, member)
+ for member in addMembers:
+ yield self._add_to_db_one(principalUID, member)
+
+ @inlineCallbacks
def removeGroup(self, principalUID):
"""
Remove a group membership record.
@@ -427,34 +498,95 @@
@param principalUID: the UID of the group principal to remove.
"""
- self._delete_from_db(principalUID)
- self._db_commit()
+ # Need to get the members before we do the delete
+ members = yield self.getMembers(principalUID)
+
+ yield self._delete_from_db(principalUID)
# Update cache
- members = yield self.getMembers(principalUID)
if members:
for member in members:
yield self._memcacher.deleteMembership(member)
yield self._memcacher.deleteMember(principalUID)
@inlineCallbacks
+ def removePrincipal(self, principalUID, delay=None):
+ """
+ Remove a group membership record.
+
+ @param principalUID: the UID of the principal to remove.
+ """
+
+ if delay:
+ # We are going to remove the principal only after <delay> seconds
+ # has passed since we first chose to remove it, to protect against
+ # transient directory problems.
+ # If <delay> is specified, first see if there was a timer set
+ # previously. If the timer is more than delay seconds old, we
+ # go ahead and remove the principal. Otherwise, do nothing.
+
+ overdue = yield self._memcacher.checkDeletionTimer(principalUID)
+
+ if overdue == False:
+ # Do nothing
+ returnValue(None)
+
+ elif overdue is None:
+ # No timer was previously set
+ self.log_debug("Delaying removal of missing proxy principal '%s'" %
+ (principalUID,))
+ yield self._memcacher.setDeletionTimer(principalUID, delay=delay)
+ returnValue(None)
+
+ self.log_warn("Removing missing proxy principal for '%s'" %
+ (principalUID,))
+
+ for suffix in ("calendar-proxy-read", "calendar-proxy-write",):
+ groupUID = "%s#%s" % (principalUID, suffix,)
+ yield self._delete_from_db(groupUID)
+
+ # Update cache
+ members = yield self.getMembers(groupUID)
+ if members:
+ for member in members:
+ yield self._memcacher.deleteMembership(member)
+ yield self._memcacher.deleteMember(groupUID)
+
+ memberships = (yield self.getMemberships(principalUID))
+ for groupUID in memberships:
+ yield self._memcacher.deleteMember(groupUID)
+
+ yield self._delete_from_db_member(principalUID)
+ yield self._memcacher.deleteMembership(principalUID)
+ yield self._memcacher.clearDeletionTimer(principalUID)
+
+ def refreshPrincipal(self, principalUID):
+ """
+ Bring back to life a principal that was previously deleted.
+
+ @param principalUID:
+ @type principalUID:
+ """
+
+ return self._memcacher.clearDeletionTimer(principalUID)
+
+ @inlineCallbacks
def getMembers(self, principalUID):
"""
Return the list of group member UIDs for the specified principal.
-
+
@return: a deferred returning a C{set} of members.
"""
+ @inlineCallbacks
def _members():
- members = set()
- for row in self._db_execute("select MEMBER from GROUPS where GROUPNAME = :1", principalUID):
- members.add(row[0])
- return members
+ result = set([row[0] for row in (yield self.query("select MEMBER from GROUPS where GROUPNAME = :1", (principalUID,)))])
+ returnValue(result)
# Pull from cache
result = yield self._memcacher.getMembers(principalUID)
if result is None:
- result = _members()
+ result = (yield _members())
yield self._memcacher.setMembers(principalUID, result)
returnValue(result)
@@ -466,19 +598,19 @@
@return: a deferred returning a C{set} of memberships.
"""
+ @inlineCallbacks
def _members():
- members = set()
- for row in self._db_execute("select GROUPNAME from GROUPS where MEMBER = :1", principalUID):
- members.add(row[0])
- return members
+ result = set([row[0] for row in (yield self.query("select GROUPNAME from GROUPS where MEMBER = :1", (principalUID,)))])
+ returnValue(result)
# Pull from cache
result = yield self._memcacher.getMemberships(principalUID)
if result is None:
- result = _members()
+ result = (yield _members())
yield self._memcacher.setMemberships(principalUID, result)
returnValue(result)
+ @inlineCallbacks
def _add_to_db(self, principalUID, members):
"""
Insert the specified entry into the database.
@@ -487,34 +619,66 @@
@param members: a list of UIDs or principals that are members of this group.
"""
for member in members:
- self._db_execute(
+ yield self.execute(
"""
insert into GROUPS (GROUPNAME, MEMBER)
values (:1, :2)
- """, principalUID, member
+ """, (principalUID, member,)
)
+ def _add_to_db_one(self, principalUID, memberUID):
+ """
+ Insert the specified entry into the database.
+
+ @param principalUID: the UID of the group principal to add.
+ @param memberUID: the UID of the principal that is being added as a member of this group.
+ """
+ return self.execute(
+ """
+ insert into GROUPS (GROUPNAME, MEMBER)
+ values (:1, :2)
+ """, (principalUID, memberUID,)
+ )
+
def _delete_from_db(self, principalUID):
"""
Deletes the specified entry from the database.
@param principalUID: the UID of the group principal to remove.
"""
- self._db_execute("delete from GROUPS where GROUPNAME = :1", principalUID)
+ return self.execute("delete from GROUPS where GROUPNAME = :1", (principalUID,))
+ def _delete_from_db_one(self, principalUID, memberUID):
+ """
+ Deletes the specified entry from the database.
+
+ @param principalUID: the UID of the group principal to remove.
+ @param memberUID: the UID of the principal that is being removed as a member of this group.
+ """
+ return self.execute("delete from GROUPS where GROUPNAME = :1 and MEMBER = :2", (principalUID, memberUID,))
+
+ def _delete_from_db_member(self, principalUID):
+ """
+ Deletes the specified member entry from the database.
+
+ @param principalUID: the UID of the member principal to remove.
+ """
+ return self.execute("delete from GROUPS where MEMBER = :1", (principalUID,))
+
def _db_version(self):
"""
@return: the schema version assigned to this index.
"""
- return CalendarUserProxyDatabase.dbFormatVersion
+ return ProxyDB.schema_version
def _db_type(self):
"""
@return: the collection type assigned to this index.
"""
- return CalendarUserProxyDatabase.dbType
+ return ProxyDB.schema_type
- def _db_init_data_tables(self, q):
+ @inlineCallbacks
+ def _db_init_data_tables(self):
"""
Initialise the underlying database tables.
@param q: a database cursor to use.
@@ -523,46 +687,90 @@
#
# GROUPS table
#
- q.execute(
+ yield self._create_table("GROUPS", (
+ ("GROUPNAME", "text"),
+ ("MEMBER", "text"),
+ ))
+
+ yield self._db_execute(
"""
- create table GROUPS (
- GROUPNAME text,
- MEMBER text
- )
- """
- )
- q.execute(
- """
create index GROUPNAMES on GROUPS (GROUPNAME)
"""
)
- q.execute(
+ yield self._db_execute(
"""
create index MEMBERS on GROUPS (MEMBER)
"""
)
- def _db_upgrade_data_tables(self, q, old_version):
+ @inlineCallbacks
+ def _db_upgrade_data_tables(self, old_version):
"""
Upgrade the data from an older version of the DB.
- @param q: a database cursor to use.
@param old_version: existing DB's version number
@type old_version: str
"""
# Add index if old version is less than "4"
if int(old_version) < 4:
- q.execute(
+ yield self._db_execute(
"""
create index GROUPNAMES on GROUPS (GROUPNAME)
"""
)
- q.execute(
+ yield self._db_execute(
"""
create index MEMBERS on GROUPS (MEMBER)
"""
)
+ def _db_empty_data_tables(self):
+ """
+ Empty the underlying database tables.
+ @param q: a database cursor to use.
+ """
+
+ #
+ # GROUPS table
+ #
+ return self._db_execute("delete from GROUPS")
+
+ @inlineCallbacks
+ def clean(self):
+
+ if not self.initialized:
+ yield self.open()
+
+ for group in [row[0] for row in (yield self.query("select GROUPNAME from GROUPS"))]:
+ self.removeGroup(group)
+
+ yield super(ProxyDB, self).clean()
+
+
+ProxyDBService = None # Global proxyDB service
+
+
+class ProxySqliteDB(ADBAPISqliteMixin, ProxyDB):
+ """
+ Sqlite based proxy database implementation.
+ """
+
+ def __init__(self, dbpath):
+
+ ADBAPISqliteMixin.__init__(self)
+ ProxyDB.__init__(self, "Proxies", "sqlite3", (dbpath,))
+
+class ProxyPostgreSQLDB(ADBAPIPostgreSQLMixin, ProxyDB):
+ """
+ PostgreSQL based augment database implementation.
+ """
+
+ def __init__(self, host, database, user=None, password=None):
+
+ ADBAPIPostgreSQLMixin.__init__(self, )
+ ProxyDB.__init__(self, "Proxies", "pgdb", (), host=host, database=database, user=user, password=password)
+
+
##
# Utilities
##
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxyloader.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/calendaruserproxyloader.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxyloader.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/calendaruserproxyloader.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,137 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.parsers.expat import ExpatError
+from twistedcaldav.directory import calendaruserproxy
+from twisted.internet.defer import inlineCallbacks
+import types
+
+"""
+XML based calendar user proxy loader.
+"""
+
+__all__ = [
+ "XMLCalendarUserProxyLoader",
+]
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_PROXIES = "proxies"
+ELEMENT_RECORD = "record"
+
+ELEMENT_GUID = "guid"
+ELEMENT_PROXIES = "proxies"
+ELEMENT_READ_ONLY_PROXIES = "read-only-proxies"
+ELEMENT_MEMBER = "member"
+
+ATTRIBUTE_REPEAT = "repeat"
+
+class XMLCalendarUserProxyLoader(object):
+ """
+ XML calendar user proxy configuration file parser and loader.
+ """
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+ def __init__(self, xmlFile):
+
+ self.items = []
+ self.xmlFile = xmlFile
+
+ # Read in XML
+ try:
+ tree = ElementTree(file=self.xmlFile)
+ except ExpatError, e:
+ log.error("Unable to parse file '%s' because: %s" % (self.xmlFile, e,), raiseException=RuntimeError)
+
+ # Verify that top-level element is correct
+ proxies_node = tree.getroot()
+ if proxies_node.tag != ELEMENT_PROXIES:
+ log.error("Ignoring file '%s' because it is not a proxies file" % (self.xmlFile,), raiseException=RuntimeError)
+
+ self._parseXML(proxies_node)
+
+ def _parseXML(self, rootnode):
+ """
+ Parse the XML root node from the augments configuration document.
+ @param rootnode: the L{Element} to parse.
+ """
+ for child in rootnode.getchildren():
+
+ if child.tag != ELEMENT_RECORD:
+ log.error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, self.xmlFile,), raiseException=RuntimeError)
+
+ repeat = int(child.get(ATTRIBUTE_REPEAT, "1"))
+
+ guid = None
+ write_proxies = set()
+ read_proxies = set()
+ for node in child.getchildren():
+
+ if node.tag == ELEMENT_GUID:
+ guid = node.text
+
+ elif node.tag in (
+ ELEMENT_PROXIES,
+ ELEMENT_READ_ONLY_PROXIES,
+ ):
+ self._parseMembers(node, write_proxies if node.tag == ELEMENT_PROXIES else read_proxies)
+ else:
+ log.error("Invalid element '%s' in proxies file: '%s'" % (node.tag, self.xmlFile,), raiseException=RuntimeError)
+
+ # Must have at least a guid
+ if not guid:
+ log.error("Invalid record '%s' without a guid in proxies file: '%s'" % (child, self.xmlFile,), raiseException=RuntimeError)
+
+ if repeat > 1:
+ for i in xrange(1, repeat+1):
+ self._buildRecord(guid, write_proxies, read_proxies, i)
+ else:
+ self._buildRecord(guid, write_proxies, read_proxies)
+
+ def _parseMembers(self, node, addto):
+ for child in node.getchildren():
+ if child.tag == ELEMENT_MEMBER:
+ addto.add(child.text)
+
+ def _buildRecord(self, guid, write_proxies, read_proxies, count=None):
+
+ def expandCount(value, count):
+
+ if type(value) in types.StringTypes:
+ return value % (count,) if count and "%" in value else value
+ else:
+ return value
+
+ guid = expandCount(guid, count)
+ write_proxies = set([expandCount(member, count) for member in write_proxies])
+ read_proxies = set([expandCount(member, count) for member in read_proxies])
+
+ self.items.append((guid, write_proxies, read_proxies,))
+
+ @inlineCallbacks
+ def updateProxyDB(self):
+
+ db = calendaruserproxy.ProxyDBService
+ for item in self.items:
+ guid, write_proxies, read_proxies = item
+ for proxy in write_proxies:
+ yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-write"), (proxy,))
+ for proxy in read_proxies:
+ yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-read"), (proxy,))
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/directory.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/directory.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -34,9 +34,11 @@
from twisted.cred.checkers import ICredentialsChecker
from twisted.web2.dav.auth import IPrincipalCredentials
+from twistedcaldav.config import config
from twistedcaldav.log import LoggingMixIn
from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
from twistedcaldav.directory.util import uuidFromName
+from twistedcaldav.partitions import partitions
class DirectoryService(LoggingMixIn):
implements(IDirectoryService, ICredentialsChecker)
@@ -151,19 +153,20 @@
implements(IDirectoryRecord)
def __repr__(self):
- return "<%s[%s@%s(%s)] %s(%s) %r>" % (
+ return "<%s[%s@%s(%s)] %s(%s) %r @ %s>" % (
self.__class__.__name__,
self.recordType,
self.service.guid,
self.service.realmName,
self.guid,
self.shortName,
- self.fullName
+ self.fullName,
+ self.hostedAt,
)
def __init__(
- self, service, recordType, guid, shortName, fullName,
- calendarUserAddresses, autoSchedule, enabledForCalendaring=True,
+ self, service, recordType, guid,
+ shortName, fullName, emailAddresses,
):
assert service.realmName is not None
assert recordType
@@ -172,19 +175,17 @@
if not guid:
guid = uuidFromName(service.guid, "%s:%s" % (recordType, shortName))
- if enabledForCalendaring:
- calendarUserAddresses.add("urn:uuid:%s" % (guid,))
- else:
- assert len(calendarUserAddresses) == 0
-
self.service = service
self.recordType = recordType
self.guid = guid
+ self.enabled = False
+ self.hostedAt = ""
self.shortName = shortName
self.fullName = fullName
- self.enabledForCalendaring = enabledForCalendaring
- self.calendarUserAddresses = calendarUserAddresses
- self.autoSchedule = autoSchedule
+ self.emailAddresses = emailAddresses
+ self.enabledForCalendaring = False
+ self.autoSchedule = False
+ self.calendarUserAddresses = set()
def __cmp__(self, other):
if not isinstance(other, DirectoryRecord):
@@ -199,35 +200,52 @@
def __hash__(self):
h = hash(self.__class__)
for attr in ("service", "recordType", "shortName", "guid",
- "enabledForCalendaring"):
+ "enabled", "enabledForCalendaring"):
h = (h + hash(getattr(self, attr))) & sys.maxint
return h
- def members(self):
- return ()
+ def addAugmentInformation(self, augment):
+
+ if augment:
+ self.enabled = augment.enabled
+ self.hostedAt = augment.hostedAt
+ self.enabledForCalendaring = augment.enabledForCalendaring
+ self.autoSchedule = augment.autoSchedule
+ self.calendarUserAddresses = set(augment.calendarUserAddresses)
- def groups(self):
- return ()
+ if self.enabledForCalendaring and self.recordType == self.service.recordType_groups:
+ self.log_error("Group '%s(%s)' cannot be enabled for calendaring" % (self.guid, self.shortName,))
+ self.enabledForCalendaring = False
+
+ if self.enabledForCalendaring:
+ for email in self.emailAddresses:
+ self.calendarUserAddresses.add("mailto:%s" % (email.lower(),))
+ self.calendarUserAddresses.add("urn:uuid:%s" % (self.guid,))
+ else:
+ assert len(self.calendarUserAddresses) == 0
- def proxies(self):
- return ()
+ else:
+ self.enabled = False
+ self.hostedAt = ""
+ self.enabledForCalendaring = False
+ self.calendarUserAddresses = set()
- def proxyFor(self):
+ def members(self):
return ()
- def readOnlyProxies(self):
+ def groups(self):
return ()
- def readOnlyProxyFor(self):
- return ()
-
- def hasEditableProxyMembership(self):
- return self.recordType in (DirectoryService.recordType_users, DirectoryService.recordType_groups)
-
def verifyCredentials(self, credentials):
return False
+ def locallyHosted(self):
+ return not self.hostedAt or not config.Partitioning.Enabled or self.hostedAt == config.Partitioning.ServerPartitionID
+
+ def hostedURL(self):
+ return partitions.getPartitionURL(self.hostedAt)
+
class DirectoryError(RuntimeError):
"""
Generic directory error.
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/idirectory.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/idirectory.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -79,11 +79,13 @@
service = Attribute("The L{IDirectoryService} this record exists in.")
recordType = Attribute("The type of this record.")
guid = Attribute("The GUID of this record.")
+ enabled = Attribute("Determines whether this record should be provisioned as a principal.")
+ hostedAt = Attribute("Identifies the server that actually hosts data for the record.")
shortName = Attribute("The name of this record.")
fullName = Attribute("The full name of this record.")
+ emailAddress = Attribute("The email address of this record.")
+ enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
calendarUserAddresses = Attribute("A set of calendar user addresses for this record.")
- autoSchedule = Attribute("Principal identified by this record should automatically accept/deny meetings.")
- enabledForCalendaring = Attribute("Determines whether this record should be provisioned with a calendar home.")
def members():
"""
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/principal.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/principal.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -45,7 +45,7 @@
from twistedcaldav.config import config
from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory import calendaruserproxy
from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyPrincipalResource
from twistedcaldav.directory.directory import DirectoryService
from twistedcaldav.directory.util import NotFilePath
@@ -116,7 +116,7 @@
raise NotImplementedError("Subclass must implement principalForUID()")
def principalForRecord(self, record):
- if record is None:
+ if record is None or not record.enabled:
return None
return self.principalForUID(record.guid)
@@ -145,7 +145,7 @@
return self.getChild(uidsResourceName).getChild(uid)
def _principalForURI(self, uri):
- scheme, netloc, path, params, query, fragment = urlparse(uri)
+ scheme, netloc, path, _ignore_params, _ignore_query, _ignore_fragment = urlparse(uri)
if scheme == "":
pass
@@ -203,7 +203,7 @@
# Next try looking it up in the directory
record = self.directory.recordWithCalendarUserAddress(address)
- if record is not None:
+ if record is not None and record.enabled:
return self.principalForRecord(record)
log.debug("No principal for calendar user address: %r" % (address,))
@@ -275,7 +275,7 @@
def listChildren(self):
if config.EnablePrincipalListings:
- return (record.shortName for record in self.directory.listRecords(self.recordType))
+ return (record.shortName for record in self.directory.listRecords(self.recordType) if record.enabled)
else:
# Not a listable collection
raise HTTPError(responsecode.FORBIDDEN)
@@ -332,7 +332,7 @@
record = self.directory.recordWithGUID(primaryUID)
- if record is None:
+ if record is None or not record.enabled:
log.err("No principal found for UID: %s" % (name,))
return None
@@ -370,7 +370,7 @@
"""
super(DirectoryPrincipalResource, self).__init__(NotFilePath(isdir=True))
- self.cacheNotifier = self.cacheNotifierFactory(self)
+ self.cacheNotifier = self.cacheNotifierFactory(self, cacheHandle="PrincipalToken")
if self.isCollection():
slash = "/"
@@ -449,6 +449,7 @@
"""Record type: %s\n""" % (self.record.recordType,),
"""Short name: %s\n""" % (self.record.shortName,),
"""Full name: %s\n""" % (self.record.fullName,),
+ """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()),
@@ -481,16 +482,11 @@
"""
Return the SQL database for calendar user proxies.
- @return: the L{CalendarUserProxyDatabase} for the principal collection.
+ @return: the L{ProxyDB} for the principal collection.
"""
- # Get the principal collection we are contained in
- pcollection = self.parent.parent
-
# The db is located in the principal collection root
- if not hasattr(pcollection, "calendar_user_proxy_db"):
- setattr(pcollection, "calendar_user_proxy_db", CalendarUserProxyDatabase(config.DataRoot))
- return pcollection.calendar_user_proxy_db
+ return calendaruserproxy.ProxyDBService
def alternateURIs(self):
# FIXME: Add API to IDirectoryRecord for getting a record URI?
@@ -512,7 +508,6 @@
if record not in records:
records.add(record)
- myRecordType = self.record.recordType
for relative in getattr(record, method)():
if relative not in records:
found = self.parent.principalForRecord(relative)
@@ -538,10 +533,6 @@
groups = self._getRelatives("groups")
if config.EnableProxyPrincipals:
- # Get any directory specified proxies
- groups.update(self._getRelatives("proxyFor", proxy='read-write'))
- groups.update(self._getRelatives("readOnlyProxyFor", proxy='read-only'))
-
# Get proxy group UIDs and map to principal resources
proxies = []
d = waitForDeferred(self._calendar_user_proxy_index().getMemberships(self.principalUID()))
@@ -551,6 +542,10 @@
subprincipal = self.parent.principalForUID(uid)
if subprincipal:
proxies.append(subprincipal)
+ else:
+ d = waitForDeferred(self._calendar_user_proxy_index().removeGroup(uid))
+ yield d
+ d.getResult()
groups.update(proxies)
@@ -569,13 +564,6 @@
proxyFors.update(results)
if config.EnableProxyPrincipals:
- # Get any directory specified proxies
- if read_write:
- directoryProxies = self._getRelatives("proxyFor", proxy='read-write')
- else:
- directoryProxies = self._getRelatives("readOnlyProxyFor", proxy='read-only')
- proxyFors.update([subprincipal.parent for subprincipal in directoryProxies])
-
# Get proxy group UIDs and map to principal resources
proxies = []
d = waitForDeferred(self._calendar_user_proxy_index().getMemberships(self.principalUID()))
@@ -588,6 +576,13 @@
proxyFors.update(proxies)
+ uids = set()
+ for principal in tuple(proxyFors):
+ if principal.principalUID() in uids:
+ proxyFors.remove(principal)
+ else:
+ uids.add(principal.principalUID())
+
yield proxyFors
def principalCollections(self):
@@ -596,7 +591,23 @@
def principalUID(self):
return self.record.guid
+ def locallyHosted(self):
+ return self.record.locallyHosted()
+
+ def hostedURL(self):
+ return self.record.hostedURL()
+
##
+ # Extra resource info
+ ##
+
+ def setAutoSchedule(self, autoSchedule):
+ self.record.autoSchedule = autoSchedule
+
+ def getAutoSchedule(self):
+ return self.record.autoSchedule
+
+ ##
# Static
##
@@ -699,15 +710,6 @@
def autoSchedule(self):
return self.record.autoSchedule
- def proxies(self):
- return self._getRelatives("proxies")
-
- def readOnlyProxies(self):
- return self._getRelatives("readOnlyProxies")
-
- def hasEditableProxyMembership(self):
- return self.record.hasEditableProxyMembership()
-
def scheduleInbox(self, request):
home = self._calendarHome()
if home is None:
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sqldb.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sqldb.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,377 +0,0 @@
-##
-# Copyright (c) 2006-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-SQL (sqlite) based user/group/resource directory service implementation.
-"""
-
-"""
-SCHEMA:
-
-User Database:
-
-ROW: RECORD_TYPE, SHORT_NAME (unique), PASSWORD, NAME
-
-Group Database:
-
-ROW: SHORT_NAME, MEMBER_SHORT_NAME
-
-CUAddress database:
-
-ROW: ADDRESS (unqiue), SHORT_NAME
-
-"""
-
-__all__ = [
- "SQLDirectoryService",
-]
-
-from twisted.cred.credentials import UsernamePassword
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
-from twistedcaldav.sql import AbstractSQLDatabase
-from twistedcaldav.sql import db_prefix
-
-import os
-
-class SQLDirectoryManager(AbstractSQLDatabase):
- """
- House keeping operations on the SQL DB, including loading from XML file,
- and record dumping. This can be used as a standalong DB management tool.
- """
- dbType = "DIRECTORYSERVICE"
- dbFilename = db_prefix + "accounts"
- dbFormatVersion = "3"
-
- def __init__(self, path):
- path = os.path.join(path, SQLDirectoryManager.dbFilename)
- super(SQLDirectoryManager, self).__init__(path, True)
-
- def loadFromXML(self, xmlFile):
- parser = XMLAccountsParser(xmlFile)
-
- # Totally wipe existing DB and start from scratch
- if os.path.exists(self.dbpath):
- os.remove(self.dbpath)
-
- self._db_execute("insert into SERVICE (REALM) values (:1)", parser.realm)
-
- # Now add records to db
- for item in parser.items.values():
- for entry in item.itervalues():
- self._add_to_db(entry)
- self._db_commit()
-
- def getRealm(self):
- for realm in self._db_execute("select REALM from SERVICE"):
- return realm[0].decode("utf-8")
- else:
- return ""
-
- def listRecords(self, recordType):
- # Get each account record
- for shortName, guid, password, name in self._db_execute(
- """
- select SHORT_NAME, GUID, PASSWORD, NAME
- from ACCOUNTS
- where RECORD_TYPE = :1
- """, recordType
- ):
- # See if we have members
- members = self.members(shortName)
-
- # See if we are a member of any groups
- groups = self.groups(shortName)
-
- # Get calendar user addresses
- calendarUserAddresses = self.calendarUserAddresses(shortName)
-
- # TODO: need this for Resources and Locations
- autoSchedule = False
-
- yield shortName, guid, password, name, members, groups, calendarUserAddresses, autoSchedule
-
- def getRecord(self, recordType, shortName):
- # Get individual account record
- for shortName, guid, password, name in self._db_execute(
- """
- select SHORT_NAME, GUID, PASSWORD, NAME
- from ACCOUNTS
- where RECORD_TYPE = :1
- and SHORT_NAME = :2
- """, recordType, shortName
- ):
- break
- else:
- return None
-
- # See if we have members
- members = self.members(shortName)
-
- # See if we are a member of any groups
- groups = self.groups(shortName)
-
- # Get calendar user addresses
- calendarUserAddresses = self.calendarUserAddresses(shortName)
-
- # TODO: need this for Resources and Locations
- autoSchedule = False
-
- return shortName, guid, password, name, members, groups, calendarUserAddresses, autoSchedule
-
- def members(self, shortName):
- members = set()
- for member in self._db_execute(
- """
- select MEMBER_RECORD_TYPE, MEMBER_SHORT_NAME
- from GROUPS
- where SHORT_NAME = :1
- """, shortName
- ):
- members.add(tuple(member))
- return members
-
- def groups(self, shortName):
- groups = set()
- for (name,) in self._db_execute(
- """
- select SHORT_NAME
- from GROUPS
- where MEMBER_SHORT_NAME = :1
- """, shortName
- ):
- groups.add(name)
- return groups
-
- def calendarUserAddresses(self, shortName):
- calendarUserAddresses = set()
- for (address,) in self._db_execute(
- """
- select ADDRESS
- from ADDRESSES
- where SHORT_NAME = :1
- """, shortName
- ):
- calendarUserAddresses.add(address)
- return calendarUserAddresses
-
- def _add_to_db(self, record):
- # Do regular account entry
- recordType = record.recordType
- shortName = record.shortName
- guid = record.guid
- password = record.password
- name = record.name
-
- self._db_execute(
- """
- insert into ACCOUNTS (RECORD_TYPE, SHORT_NAME, GUID, PASSWORD, NAME)
- values (:1, :2, :3, :4, :5)
- """, recordType, shortName, guid, password, name
- )
-
- # Check for members
- for memberRecordType, memberShortName in record.members:
- self._db_execute(
- """
- insert into GROUPS (SHORT_NAME, MEMBER_RECORD_TYPE, MEMBER_SHORT_NAME)
- values (:1, :2, :3)
- """, shortName, memberRecordType, memberShortName
- )
-
- # CUAddress
- for cuaddr in record.calendarUserAddresses:
- self._db_execute(
- """
- insert into ADDRESSES (ADDRESS, SHORT_NAME)
- values (:1, :2)
- """, cuaddr, shortName
- )
-
- def _delete_from_db(self, shortName):
- """
- Deletes the specified entry from all dbs.
- @param name: the name of the resource to delete.
- @param shortName: the short name of the resource to delete.
- """
- self._db_execute("delete from ACCOUNTS where SHORT_NAME = :1", shortName)
- self._db_execute("delete from GROUPS where SHORT_NAME = :1", shortName)
- self._db_execute("delete from GROUPS where MEMBER_SHORT_NAME = :1", shortName)
- self._db_execute("delete from ADDRESSES where SHORT_NAME = :1", shortName)
-
- def _db_version(self):
- """
- @return: the schema version assigned to this index.
- """
- return SQLDirectoryManager.dbFormatVersion
-
- def _db_type(self):
- """
- @return: the collection type assigned to this index.
- """
- return SQLDirectoryManager.dbType
-
- def _db_init_data_tables(self, q):
- """
- Initialise the underlying database tables.
- @param q: a database cursor to use.
- """
- #
- # SERVICE table
- #
- q.execute("create table SERVICE (REALM text)")
-
- #
- # ACCOUNTS table
- #
- q.execute(
- """
- create table ACCOUNTS (
- RECORD_TYPE text,
- SHORT_NAME text,
- GUID text,
- PASSWORD text,
- NAME text
- )
- """
- )
-
- #
- # GROUPS table
- #
- q.execute(
- """
- create table GROUPS (
- SHORT_NAME text,
- MEMBER_RECORD_TYPE text,
- MEMBER_SHORT_NAME text
- )
- """
- )
-
- #
- # ADDRESSES table
- #
- q.execute(
- """
- create table ADDRESSES (
- ADDRESS text unique,
- SHORT_NAME text
- )
- """
- )
-
-class SQLDirectoryService(DirectoryService):
- """
- XML based implementation of L{IDirectoryService}.
- """
- baseGUID = "8256E464-35E0-4DBB-A99C-F0E30C231675"
- realmName = None
-
- def __repr__(self):
- return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.manager.dbpath)
-
- def __init__(self, dbParentPath, xmlFile=None):
- super(SQLDirectoryService, self).__init__()
-
- if type(dbParentPath) is str:
- dbParentPath = FilePath(dbParentPath)
-
- self.manager = SQLDirectoryManager(dbParentPath.path)
- if xmlFile:
- self.manager.loadFromXML(xmlFile)
- self.realmName = self.manager.getRealm()
-
- def recordTypes(self):
- recordTypes = (
- DirectoryService.recordType_users,
- DirectoryService.recordType_groups,
- DirectoryService.recordType_locations,
- DirectoryService.recordType_resources,
- )
- return recordTypes
-
- def listRecords(self, recordType):
- for result in self.manager.listRecords(recordType):
- yield SQLDirectoryRecord(
- service = self,
- recordType = recordType,
- shortName = result[0],
- guid = result[1],
- password = result[2],
- name = result[3],
- members = result[4],
- groups = result[5],
- calendarUserAddresses = result[6],
- autoSchedule = result[7],
- )
-
- def recordWithShortName(self, recordType, shortName):
- result = self.manager.getRecord(recordType, shortName)
- if result:
- return SQLDirectoryRecord(
- service = self,
- recordType = recordType,
- shortName = result[0],
- guid = result[1],
- password = result[2],
- name = result[3],
- members = result[4],
- groups = result[5],
- calendarUserAddresses = result[6],
- autoSchedule = result[7],
- )
-
- return None
-
-class SQLDirectoryRecord(DirectoryRecord):
- """
- XML based implementation implementation of L{IDirectoryRecord}.
- """
- def __init__(self, service, recordType, shortName, guid, password, name, members, groups, calendarUserAddresses, autoSchedule):
- super(SQLDirectoryRecord, self).__init__(
- service = service,
- recordType = recordType,
- guid = guid,
- shortName = shortName,
- fullName = name,
- calendarUserAddresses = calendarUserAddresses,
- autoSchedule = autoSchedule,
- )
-
- self.password = password
- self._members = members
- self._groups = groups
-
- def members(self):
- for recordType, shortName in self._members:
- yield self.service.recordWithShortName(recordType, shortName)
-
- def groups(self):
- for shortName in self._groups:
- yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
-
- def verifyCredentials(self, credentials):
- if isinstance(credentials, UsernamePassword):
- return credentials.password == self.password
-
- return super(SQLDirectoryRecord, self).verifyCredentials(credentials)
-
-if __name__ == '__main__':
- mgr = SQLDirectoryManager("./")
- mgr.loadFromXML("test/accounts.xml")
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sudo.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sudo.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/sudo.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -132,12 +132,13 @@
guid=None,
shortName=shortName,
fullName=shortName,
- calendarUserAddresses=set(),
- autoSchedule=False,
- enabledForCalendaring=False)
+ emailAddresses=set(),
+ )
self.password = entry['password']
+ self.enabled = True # Explicitly enabled
+
def verifyCredentials(self, credentials):
if IUsernamePassword.providedBy(credentials):
return credentials.checkPassword(self.password)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/accounts.xml 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/accounts.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
-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.
@@ -16,7 +16,7 @@
limitations under the License.
-->
-<!DOCTYPE accounts SYSTEM "../../../conf/accounts.dtd">
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
<accounts realm="Test">
<user>
@@ -30,36 +30,45 @@
<guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
<password>zehcnasw</password>
<name>Wilfredo Sanchez</name>
- <cuaddr>mailto:wsanchez at example.com</cuaddr>
+ <email-address>wsanchez at example.com</email-address>
</user>
<user>
<uid>cdaboo</uid>
<guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
<password>oobadc</password>
<name>Cyrus Daboo</name>
- <cuaddr>mailto:cdaboo at example.com</cuaddr>
+ <email-address>cdaboo at example.com</email-address>
</user>
<user>
<uid>lecroy</uid>
<guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
<password>yorcel</password>
<name>Chris Lecroy</name>
- <cuaddr>mailto:lecroy at example.com</cuaddr>
+ <email-address>lecroy at example.com</email-address>
</user>
<user>
<uid>dreid</uid>
<guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
<password>dierd</password>
<name>David Reid</name>
- <cuaddr>mailto:dreid at example.com</cuaddr>
+ <email-address>dreid at example.com</email-address>
</user>
+ <user>
+ <uid>nocalendar</uid>
+ <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+ <password>radnelacon</password>
+ <name>No Calendar</name>
+ <email-address>nocalendar at example.com</email-address>
+ </user>
<user repeat="2">
<uid>user%02d</uid>
+ <guid>user%02d</guid>
<password>%02duser</password>
<name>User %02d</name>
</user>
<group>
<uid>managers</uid>
+ <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
<password>managers</password>
<name>Managers</name>
<members>
@@ -68,6 +77,7 @@
</group>
<group>
<uid>admin</uid>
+ <guid>admin</guid>
<password>admin</password>
<name>Administrators</name>
<members>
@@ -76,6 +86,7 @@
</group>
<group>
<uid>grunts</uid>
+ <guid>grunts</guid>
<password>grunts</password>
<name>We do all the work</name>
<members>
@@ -86,6 +97,7 @@
</group>
<group>
<uid>right_coast</uid>
+ <guid>right_coast</guid>
<password>right_coast</password>
<name>East Coast</name>
<members>
@@ -94,6 +106,7 @@
</group>
<group>
<uid>left_coast</uid>
+ <guid>left_coast</guid>
<password>left_coast</password>
<name>West Coast</name>
<members>
@@ -104,6 +117,7 @@
</group>
<group>
<uid>both_coasts</uid>
+ <guid>both_coasts</guid>
<password>both_coasts</password>
<name>Both Coasts</name>
<members>
@@ -113,6 +127,7 @@
</group>
<group>
<uid>recursive1_coasts</uid>
+ <guid>recursive1_coasts</guid>
<password>recursive1_coasts</password>
<name>Recursive1 Coasts</name>
<members>
@@ -122,6 +137,7 @@
</group>
<group>
<uid>recursive2_coasts</uid>
+ <guid>recursive2_coasts</guid>
<password>recursive2_coasts</password>
<name>Recursive2 Coasts</name>
<members>
@@ -131,74 +147,61 @@
</group>
<group>
<uid>non_calendar_group</uid>
+ <guid>non_calendar_group</guid>
<password>non_calendar_group</password>
<name>Non-calendar group</name>
<members>
<member>cdaboo</member>
<member>lecroy</member>
</members>
- <disable-calendar/>
</group>
<location>
<uid>mercury</uid>
+ <guid>mercury</guid>
<password>mercury</password>
<name>Mecury Seven</name>
- <cuaddr>mailto:mercury at example.com</cuaddr>
- <proxies>
- <member type="groups">left_coast</member>
- </proxies>
+ <email-address>mercury at example.com</email-address>
</location>
<location>
<uid>gemini</uid>
+ <guid>gemini</guid>
<password>gemini</password>
<name>Gemini Twelve</name>
- <cuaddr>mailto:gemini at example.com</cuaddr>
- <auto-schedule/>
- <proxies>
- <member>wsanchez</member>
- </proxies>
+ <email-address>gemini at example.com</email-address>
</location>
<location>
<uid>apollo</uid>
+ <guid>apollo</guid>
<password>apollo</password>
<name>Apollo Eleven</name>
- <cuaddr>mailto:apollo at example.com</cuaddr>
- <proxies>
- <member type="groups">both_coasts</member>
- </proxies>
+ <email-address>apollo at example.com</email-address>
</location>
<location>
<uid>orion</uid>
+ <guid>orion</guid>
<password>orion</password>
<name>Orion</name>
- <cuaddr>mailto:orion at example.com</cuaddr>
- <proxies>
- <member type="groups">recursive1_coasts</member>
- </proxies>
+ <email-address>orion at example.com</email-address>
</location>
<resource>
<uid>transporter</uid>
+ <guid>transporter</guid>
<password>transporter</password>
<name>Mass Transporter</name>
- <cuaddr>mailto:transporter at example.com</cuaddr>
+ <email-address>transporter at example.com</email-address>
</resource>
<resource>
<uid>ftlcpu</uid>
+ <guid>ftlcpu</guid>
<password>ftlcpu</password>
<name>Faster-Than-Light Microprocessor</name>
- <cuaddr>mailto:ftlcpu at example.com</cuaddr>
+ <email-address>ftlcpu at example.com</email-address>
</resource>
<resource>
<uid>non_calendar_proxy</uid>
<guid>non_calendar_proxy</guid>
<password>non_calendar_proxy</password>
<name>Non-calendar proxy</name>
- <cuaddr>mailto:non_calendar_proxy at example.com</cuaddr>
- <proxies>
- <member type="groups">non_calendar_group</member>
- </proxies>
- <read-only-proxies>
- <member type="groups">recursive2_coasts</member>
- </read-only-proxies>
+ <email-address>non_calendar_proxy at example.com</email-address>
</resource>
</accounts>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test-default.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/augments-test-default.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test-default.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test-default.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/augments.dtd">
+
+<augments>
+ <record>
+ <guid>Default</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <hosted-at>00001</hosted-at>
+ </record>
+ <record>
+ <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <cuaddr>mailto:wsanchez at example.com</cuaddr>
+ </record>
+ <record>
+ <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+ <enable>false</enable>
+ </record>
+ <record>
+ <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+ <enable>true</enable>
+ <enable-calendar>false</enable-calendar>
+ </record>
+ <record>
+ <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+ <enable>true</enable>
+ <hosted-at>00001</hosted-at>
+ </record>
+ <record>
+ <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+ <enable>true</enable>
+ <hosted-at>00002</hosted-at>
+ </record>
+ <record>
+ <guid>6A73326A-F781-47E7-A9F8-AF47364D4152</guid>
+ <enable>true</enable>
+ <hosted-at>00002</hosted-at>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <auto-schedule>true</auto-schedule>
+ <cuaddr>mailto:usera at example.com</cuaddr>
+ <cuaddr>mailto:user.a at example.com</cuaddr>
+ <cuaddr>mailto:user_a at example.com</cuaddr>
+ </record>
+</augments>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/augments-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments-test.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE accounts SYSTEM "../../../conf/auth/augments.dtd">
+
+<augments>
+ <record>
+ <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <cuaddr>mailto:wsanchez at example.com</cuaddr>
+ </record>
+ <record>
+ <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+ <enable>false</enable>
+ </record>
+ <record>
+ <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+ <enable>true</enable>
+ <enable-calendar>false</enable-calendar>
+ </record>
+ <record>
+ <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+ <enable>true</enable>
+ <hosted-at>00001</hosted-at>
+ </record>
+ <record>
+ <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+ <enable>true</enable>
+ <hosted-at>00002</hosted-at>
+ </record>
+ <record>
+ <guid>6A73326A-F781-47E7-A9F8-AF47364D4152</guid>
+ <enable>true</enable>
+ <hosted-at>00002</hosted-at>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <auto-schedule>true</auto-schedule>
+ <cuaddr>mailto:usera at example.com</cuaddr>
+ <cuaddr>mailto:user.a at example.com</cuaddr>
+ <cuaddr>mailto:user_a at example.com</cuaddr>
+ </record>
+</augments>
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/augments.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/augments.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE augments SYSTEM "../../../conf/augments.dtd">
+
+<augments realm="Test">
+ <record>
+ <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+ <enable>true</enable>
+ <enable-calendar>false</enable-calendar>
+ </record>
+ <record repeat="2">
+ <guid>user%02d</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>admin</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>grunts</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>right_coast</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>left_coast</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>both_coasts</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>recursive1_coasts</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>recursive2_coasts</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>non_calendar_group</guid>
+ <enable>true</enable>
+ </record>
+ <record>
+ <guid>mercury</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>gemini</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <auto-schedule>true</auto-schedule>
+ </record>
+ <record>
+ <guid>apollo</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>orion</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>transporter</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>ftlcpu</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+ <record>
+ <guid>non_calendar_proxy</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ </record>
+</augments>
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/basic
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/basic 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/basic 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,4 +0,0 @@
-wsanchez:Cytm0Bwm7CPJs
-cdaboo:I.Ef5FJl5GVh2
-dreid:LVhqAv4qSrYPs
-lecroy:/7/5VDrkrLxY.
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/digest
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/digest 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/digest 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,4 +0,0 @@
-wsanchez:Test:decbe233ab3d997cacc2fc058b19db8c
-cdaboo:Test:61164bf3d607d072fe8a7ac420b24aac
-dreid:Test:8ee67801004b2752f72b84e7064889a6
-lecroy:Test:60d4feb424430953be045738041e51be
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/groups
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/groups 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/groups 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,4 +0,0 @@
-managers: lecroy
-grunts: wsanchez, cdaboo, dreid
-right_coast: cdaboo
-left_coast: wsanchez, dreid, lecroy
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/proxies.xml (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/proxies.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/proxies.xml (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/proxies.xml 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+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.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE proxies SYSTEM "proxies.dtd">
+
+<proxies>
+ <record>
+ <guid>mercury</guid>
+ <proxies>
+ <member>left_coast</member>
+ </proxies>
+ </record>
+ <record>
+ <guid>gemini</guid>
+ <proxies>
+ <member>6423F94A-6B76-4A3A-815B-D52CFD77935D</member>
+ </proxies>
+ </record>
+ <record>
+ <guid>apollo</guid>
+ <proxies>
+ <member>both_coasts</member>
+ </proxies>
+ </record>
+ <record>
+ <guid>orion</guid>
+ <proxies>
+ <member>recursive1_coasts</member>
+ </proxies>
+ </record>
+ <record>
+ <guid>non_calendar_proxy</guid>
+ <proxies>
+ <member>non_calendar_group</member>
+ </proxies>
+ <read-only-proxies>
+ <member>recursive2_coasts</member>
+ </read-only-proxies>
+ </record>
+</proxies>
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_aggregate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_aggregate.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_aggregate.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -14,20 +14,18 @@
# limitations under the License.
##
-from twistedcaldav.directory.apache import BasicDirectoryService
from twistedcaldav.directory.xmlfile import XMLDirectoryService
from twistedcaldav.directory.aggregate import AggregateDirectoryService
-from twistedcaldav.directory.test.test_apache import digestRealm, basicUserFile, groupFile
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
import twistedcaldav.directory.test.util
+from twistedcaldav.directory import augment
apache_prefix = "apache:"
xml_prefix = "xml:"
testServices = (
- (apache_prefix, twistedcaldav.directory.test.test_apache.Apache ),
(xml_prefix , twistedcaldav.directory.test.test_xmlfile.XMLFile),
)
@@ -65,10 +63,9 @@
"""
Returns an IDirectoryService.
"""
- apacheService = BasicDirectoryService(digestRealm, basicUserFile, groupFile)
- apacheService.recordTypePrefix = apache_prefix
-
- xmlService = XMLDirectoryService(xmlFile)
+ xmlService = XMLDirectoryService(xmlFile=xmlFile)
xmlService.recordTypePrefix = xml_prefix
- return AggregateDirectoryService((apacheService, xmlService))
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+
+ return AggregateDirectoryService((xmlService,))
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_apache.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_apache.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_apache.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,120 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import os
-
-from twisted.python.filepath import FilePath
-
-import twistedcaldav.directory.test.util
-from twistedcaldav.directory.apache import BasicDirectoryService, DigestDirectoryService
-from twistedcaldav.directory.directory import DirectoryService
-
-digestRealm = "Test"
-
-basicUserFile = FilePath(os.path.join(os.path.dirname(__file__), "basic"))
-digestUserFile = FilePath(os.path.join(os.path.dirname(__file__), "digest"))
-groupFile = FilePath(os.path.join(os.path.dirname(__file__), "groups"))
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-class Apache (object):
- recordTypes = set((
- DirectoryService.recordType_users,
- DirectoryService.recordType_groups
- ))
-
- users = {
- "wsanchez": { "password": "foo", "guid": None, "addresses": () },
- "cdaboo" : { "password": "bar", "guid": None, "addresses": () },
- "dreid" : { "password": "baz", "guid": None, "addresses": () },
- "lecroy" : { "password": "quux", "guid": None, "addresses": () },
- }
-
- groups = {
- "managers" : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "lecroy"),) },
- "grunts" : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "wsanchez"),
- (DirectoryService.recordType_users, "cdaboo"),
- (DirectoryService.recordType_users, "dreid")) },
- "right_coast": { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "cdaboo"),) },
- "left_coast" : { "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users, "wsanchez"),
- (DirectoryService.recordType_users, "dreid"),
- (DirectoryService.recordType_users, "lecroy")) },
- }
-
- locations = {
- }
-
- resources = {
- }
-
- def service(self):
- return self.serviceClass(digestRealm, self.userFile(), self.groupFile())
-
- userFileName = None
-
- def userFile(self):
- if not hasattr(self, "_userFile"):
- if self.userFileName is None:
- raise NotImplementedError("Test subclass needs to specify userFileName.")
- self._userFile = FilePath(self.mktemp())
- basicUserFile.copyTo(self._userFile)
- return self._userFile
-
- def groupFile(self):
- if not hasattr(self, "_groupFile"):
- self._groupFile = FilePath(self.mktemp())
- groupFile.copyTo(self._groupFile)
- return self._groupFile
-
- def test_changedGroupFile(self):
- self.groupFile().open("w").write("grunts: wsanchez\n")
- self.assertEquals(self.recordNames(DirectoryService.recordType_groups), set(("grunts",)))
-
- def test_recordTypes_user(self):
- """
- IDirectoryService.recordTypes(userFile)
- """
- self.assertEquals(set(self.serviceClass(digestRealm, self.userFile()).recordTypes()), set((DirectoryService.recordType_users,)))
-
- userEntry = None
-
- def test_changedUserFile(self):
- if self.userEntry is None:
- raise NotImplementedError("Test subclass needs to specify userEntry.")
- self.userFile().open("w").write(self.userEntry[1])
- self.assertEquals(self.recordNames(DirectoryService.recordType_users), set((self.userEntry[0],)))
-
-class Basic (Apache, twistedcaldav.directory.test.util.BasicTestCase):
- """
- Test Apache-Compatible UserFile/GroupFile directory implementation.
- """
- serviceClass = BasicDirectoryService
-
- userFileName = basicUserFile
- userEntry = ("wsanchez", "wsanchez:Cytm0Bwm7CPJs\n")
-
-class Digest (Apache, twistedcaldav.directory.test.util.DigestTestCase):
- """
- Test Apache-Compatible DigestFile/GroupFile directory implementation.
- """
- serviceClass = DigestDirectoryService
-
- userFileName = digestUserFile
- userEntry = ("wsanchez", "wsanchez:Test:decbe233ab3d997cacc2fc058b19db8c\n")
-
- def test_verifyCredentials_digest(self):
- raise NotImplementedError() # Use super's implementation
- test_verifyCredentials_digest.todo = "unimplemented"
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_augment.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/test/test_augment.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_augment.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_augment.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,178 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory.augment import AugmentXMLDB, AugmentSqliteDB,\
+ AugmentPostgreSQLDB
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
+import cStringIO
+import os
+
+xmlFile = os.path.join(os.path.dirname(__file__), "augments-test.xml")
+xmlFileDefault = os.path.join(os.path.dirname(__file__), "augments-test-default.xml")
+
+testRecords = (
+ {"guid":"D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "enabled":True, "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False, "calendarUserAddresses":set()},
+ {"guid":"6423F94A-6B76-4A3A-815B-D52CFD77935D", "enabled":True, "hostedAt":"", "enabledForCalendaring":True, "autoSchedule":False, "calendarUserAddresses":set(("mailto:wsanchez at example.com",))},
+ {"guid":"5A985493-EE2C-4665-94CF-4DFEA3A89500", "enabled":False, "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False, "calendarUserAddresses":set()},
+ {"guid":"8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "enabled":True, "hostedAt":"", "enabledForCalendaring":False, "autoSchedule":False, "calendarUserAddresses":set()},
+ {"guid":"5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "enabled":True, "hostedAt":"00001", "enabledForCalendaring":False, "autoSchedule":False, "calendarUserAddresses":set()},
+ {"guid":"543D28BA-F74F-4D5F-9243-B3E3A61171E5", "enabled":True, "hostedAt":"00002", "enabledForCalendaring":False, "autoSchedule":False, "calendarUserAddresses":set()},
+ {"guid":"6A73326A-F781-47E7-A9F8-AF47364D4152", "enabled":True, "hostedAt":"00002", "enabledForCalendaring":True, "autoSchedule":True, "calendarUserAddresses":set(("mailto:usera at example.com", "mailto:user.a at example.com", "mailto:user_a at example.com",))},
+)
+
+testRecordDefault = {"guid":"A4318887-F2C7-4A70-9056-B88CC8DB26F1", "enabled":True, "hostedAt":"00001", "enabledForCalendaring":True, "autoSchedule":False, "calendarUserAddresses":set()}
+
+class AugmentTests(TestCase):
+
+ @inlineCallbacks
+ def _checkRecord(self, db, items):
+
+ record = (yield db.getAugmentRecord(items["guid"]))
+ self.assertTrue(record is not None)
+
+ for k,v in items.iteritems():
+ self.assertEqual(getattr(record, k), v)
+
+ @inlineCallbacks
+ def _checkNoRecord(self, db, guid):
+
+ record = (yield db.getAugmentRecord(guid))
+ self.assertTrue(record is None)
+
+class AugmentXMLTests(AugmentTests):
+
+ @inlineCallbacks
+ def test_read(self):
+
+ db = AugmentXMLDB((xmlFile,))
+
+ for item in testRecords:
+ yield self._checkRecord(db, item)
+
+ yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+ @inlineCallbacks
+ def test_read_default(self):
+
+ db = AugmentXMLDB((xmlFileDefault,))
+
+ for item in testRecords:
+ yield self._checkRecord(db, item)
+
+ yield self._checkRecord(db, testRecordDefault)
+
+ def test_parseErrors(self):
+
+ db = {}
+ self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO(""), db)
+ self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<accounts>
+ <foo/>
+</accounts>
+"""), db)
+ self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<augments>
+ <foo/>
+</augments>
+"""), db)
+ self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+<augments>
+ <record>
+ <enable>true</enable>
+ </record>
+</augments>
+"""), db)
+ self.assertRaises(RuntimeError, XMLAugmentsParser, cStringIO.StringIO("""<?xml version="1.0" encoding="utf-8"?>
+ <record>
+ <guid>admin</guid>
+ <enable>true</enable>
+ <foo/>
+ </record>
+"""), db)
+
+class AugmentSqliteTests(AugmentTests):
+
+ @inlineCallbacks
+ def test_read(self):
+
+ db = AugmentSqliteDB(self.mktemp())
+
+ dbxml = AugmentXMLDB((xmlFile,))
+ for record in dbxml.db.values():
+ yield db.addAugmentRecord(record)
+
+ for item in testRecords:
+ yield self._checkRecord(db, item)
+
+ yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+ @inlineCallbacks
+ def test_read_default(self):
+
+ db = AugmentSqliteDB(self.mktemp())
+
+ dbxml = AugmentXMLDB((xmlFileDefault,))
+ for record in dbxml.db.values():
+ yield db.addAugmentRecord(record)
+
+ for item in testRecords:
+ yield self._checkRecord(db, item)
+
+ yield self._checkRecord(db, testRecordDefault)
+
+class AugmentPostgreSQLTests(AugmentTests):
+
+ @inlineCallbacks
+ def test_read(self):
+
+ db = AugmentPostgreSQLDB("localhost", "augments")
+ yield db.clean()
+
+ dbxml = AugmentXMLDB((xmlFile,))
+ for record in dbxml.db.values():
+ yield db.addAugmentRecord(record)
+
+ for item in testRecords:
+ yield self._checkRecord(db, item)
+
+ yield self._checkNoRecord(db, "D11F03A0-97EA-48AF-9A6C-FAC7F3975767")
+
+ @inlineCallbacks
+ def test_read_default(self):
+
+ db = AugmentPostgreSQLDB("localhost", "augments")
+ yield db.clean()
+
+ dbxml = AugmentXMLDB((xmlFileDefault,))
+ for record in dbxml.db.values():
+ yield db.addAugmentRecord(record)
+
+ for item in testRecords:
+ yield self._checkRecord(db, item)
+
+ yield self._checkRecord(db, testRecordDefault)
+
+try:
+ import pgdb
+except ImportError:
+ AugmentPostgreSQLTests.skip = True
+else:
+ try:
+ db = pgdb.connect(host="localhost", database="augments")
+ except:
+ AugmentPostgreSQLTests.skip = True
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_calendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_calendar.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_calendar.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -19,8 +19,9 @@
from twisted.web2.dav import davxml
from twisted.web2.test.test_server import SimpleRequest
+from twistedcaldav.directory import augment
from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
from twistedcaldav.directory.xmlfile import XMLDirectoryService
from twistedcaldav.static import CalendarHomeProvisioningFile
@@ -34,11 +35,12 @@
super(ProvisionedCalendars, self).setUp()
# Setup the initial directory
- self.xmlfile = self.mktemp()
- fd = open(self.xmlfile, "w")
+ self.xmlFile = self.mktemp()
+ fd = open(self.xmlFile, "w")
fd.write(open(xmlFile.path, "r").read())
fd.close()
- self.directoryService = XMLDirectoryService(self.xmlfile)
+ self.directoryService = XMLDirectoryService(xmlFile=self.xmlFile)
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
# Set up a principals hierarchy for each service we're testing with
name = "principals"
@@ -69,3 +71,33 @@
request = SimpleRequest(self.site, "GET", "/calendars/users/12345/")
d = request.locateResource(request.uri)
d.addCallback(_response)
+
+ def test_ExistentCalendarHome(self):
+
+ def _response(resource):
+ if resource is None:
+ self.fail("Incorrect response to GET on existent calendar home.")
+
+ request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/")
+ d = request.locateResource(request.uri)
+ d.addCallback(_response)
+
+ def test_ExistentCalendar(self):
+
+ def _response(resource):
+ if resource is None:
+ self.fail("Incorrect response to GET on existent calendar.")
+
+ request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/calendar/")
+ d = request.locateResource(request.uri)
+ d.addCallback(_response)
+
+ def test_ExistentInbox(self):
+
+ def _response(resource):
+ if resource is None:
+ self.fail("Incorrect response to GET on existent inbox.")
+
+ request = SimpleRequest(self.site, "GET", "/calendars/users/wsanchez/inbox/")
+ d = request.locateResource(request.uri)
+ d.addCallback(_response)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_digest.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_digest.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_digest.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -2,12 +2,12 @@
from twisted.cred import error
from twisted.internet import address
-from twistedcaldav.test.util import TestCase
from twisted.web2.auth import digest
from twisted.web2.auth.wrapper import UnauthorizedResponse
from twisted.web2.test.test_server import SimpleRequest
from twisted.web2.dav.fileop import rmdir
from twistedcaldav.directory.digest import QopDigestCredentialFactory
+from twistedcaldav.test.util import TestCase
import os
import md5
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_guidchange.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_guidchange.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_guidchange.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -37,11 +37,11 @@
super(ProvisionedPrincipals, self).setUp()
# Setup the initial directory
- self.xmlfile = self.mktemp()
- fd = open(self.xmlfile, "w")
+ self.xmlFile = self.mktemp()
+ fd = open(self.xmlFile, "w")
fd.write(open(xmlFile.path, "r").read())
fd.close()
- self.directoryService = XMLDirectoryService(self.xmlfile)
+ self.directoryService = XMLDirectoryService(xmlFile=self.xmlFile)
# Set up a principals hierarchy for each service we're testing with
name = "principals"
@@ -78,7 +78,7 @@
def privs1(result):
# Change GUID in record
- fd = open(self.xmlfile, "w")
+ fd = open(self.xmlFile, "w")
fd.write(open(xmlFile.path, "r").read().replace(oldUID, newUID))
fd.close()
fd = None
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectory.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectory.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -21,8 +21,10 @@
else:
import twisted.web2.auth.digest
import twistedcaldav.directory.test.util
+ from twistedcaldav.directory import augment
from twistedcaldav.directory.directory import DirectoryService
from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
+ import dsattributes
# Wonky hack to prevent unclean reactor shutdowns
class DummyReactor(object):
@@ -51,6 +53,7 @@
def setUp(self):
super(OpenDirectory, self).setUp()
self._service = OpenDirectoryService(node="/Search", dosetup=False)
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=())
def tearDown(self):
for call in self._service._delayedCalls:
@@ -67,12 +70,8 @@
nodeName = "/LDAPv2/127.0.0.1",
shortName = "user",
fullName = "Some user",
- calendarUserAddresses = set(("mailtoguid at example.com",)),
- autoSchedule = False,
- enabledForCalendaring = True,
+ emailAddresses = set(("someuser at example.com",)),
memberGUIDs = [],
- proxyGUIDs = (),
- readOnlyProxyGUIDs = (),
)
digestFields = {}
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryrecords.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryrecords.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryrecords.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,601 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.test.util import TestCase
-from twisted.cred.credentials import UsernamePassword
-import opendirectory
-
-
-
-try:
- from twistedcaldav.directory.appleopendirectory import OpenDirectoryService as RealOpenDirectoryService
- import dsattributes
-except ImportError:
- pass
-else:
- from twistedcaldav.directory.directory import DirectoryService
- from twistedcaldav.directory.util import uuidFromName
- from twistedcaldav.test.util import TestCase
-
- class OpenDirectoryService (RealOpenDirectoryService):
- def _queryDirectory(self, recordType):
- try:
- return self.fakerecords[recordType]
- except KeyError:
- return []
-
- class ReloadCache(TestCase):
- def setUp(self):
- super(ReloadCache, self).setUp()
- self._service = OpenDirectoryService(node="/Search", dosetup=False)
- self._service.servicetags.add("FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar")
- self._service.fakerecords = { }
-
- def tearDown(self):
- for call in self._service._delayedCalls:
- call.cancel()
-
- def _verifyRecords(self, recordType, expected):
- expected = set(expected)
- found = set(self._service._records[recordType]["records"].keys())
-
- missing = expected.difference(found)
- extras = found.difference(expected)
-
- self.assertTrue(len(missing) == 0, msg="Directory records not found: %s" % (missing,))
- self.assertTrue(len(extras) == 0, msg="Directory records not expected: %s" % (extras,))
-
- def _verifyRecordsCheckEnabled(self, recordType, expected, enabled):
- expected = set(expected)
- found = set([item for item in self._service._records[recordType]["records"].iterkeys()
- if self._service._records[recordType]["records"][item].enabledForCalendaring == enabled])
-
- missing = expected.difference(found)
- extras = found.difference(expected)
-
- self.assertTrue(len(missing) == 0, msg="Directory records not found: %s" % (missing,))
- self.assertTrue(len(extras) == 0, msg="Directory records not expected: %s" % (extras,))
-
- def _verifyDisabledRecords(self, recordType, expectedNames, expectedGUIDs):
- def check(disabledType, expected):
- expected = set(expected)
- found = self._service._records[recordType][disabledType]
-
- missing = expected.difference(found)
- extras = found.difference(expected)
-
- self.assertTrue(len(missing) == 0, msg="Disabled directory records not found: %s" % (missing,))
- self.assertTrue(len(extras) == 0, msg="Disabled directory records not expected: %s" % (extras,))
-
- check("disabled names", expectedNames)
- check("disabled guids", (guid.lower() for guid in expectedGUIDs))
-
- def test_normal(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- ],
- DirectoryService.recordType_groups: [
- fakeODRecord("Group 01"),
- fakeODRecord("Group 02"),
- ],
- DirectoryService.recordType_resources: [
- fakeODRecord("Resource 01"),
- fakeODRecord("Resource 02"),
- ],
- DirectoryService.recordType_locations: [
- fakeODRecord("Location 01"),
- fakeODRecord("Location 02"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
- self._service.reloadCache(DirectoryService.recordType_groups)
- self._service.reloadCache(DirectoryService.recordType_resources)
- self._service.reloadCache(DirectoryService.recordType_locations)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02"))
- self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
- self._verifyRecords(DirectoryService.recordType_groups, ("group01", "group02"))
- self._verifyDisabledRecords(DirectoryService.recordType_groups, (), ())
-
- self._verifyRecords(DirectoryService.recordType_resources, ("resource01", "resource02"))
- self._verifyDisabledRecords(DirectoryService.recordType_resources, (), ())
-
- self._verifyRecords(DirectoryService.recordType_locations, ("location01", "location02"))
- self._verifyDisabledRecords(DirectoryService.recordType_locations, (), ())
-
- def test_normal_disabledusers(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- fakeODRecord("User 03", addLocator=False),
- fakeODRecord("User 04", addLocator=False),
- ],
- DirectoryService.recordType_groups: [
- fakeODRecord("Group 01"),
- fakeODRecord("Group 02"),
- fakeODRecord("Group 03", addLocator=False),
- fakeODRecord("Group 04", addLocator=False),
- ],
- DirectoryService.recordType_resources: [
- fakeODRecord("Resource 01"),
- fakeODRecord("Resource 02"),
- fakeODRecord("Resource 03", addLocator=False),
- fakeODRecord("Resource 04", addLocator=False),
- ],
- DirectoryService.recordType_locations: [
- fakeODRecord("Location 01"),
- fakeODRecord("Location 02"),
- fakeODRecord("Location 03", addLocator=False),
- fakeODRecord("Location 04", addLocator=False),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
- self._service.reloadCache(DirectoryService.recordType_groups)
- self._service.reloadCache(DirectoryService.recordType_resources)
- self._service.reloadCache(DirectoryService.recordType_locations)
-
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_users, ("user01", "user02"), True)
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_users, ("user03", "user04"), False)
-
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_groups, ("group01", "group02"), True)
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_groups, ("group03", "group04"), False)
-
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_resources, ("resource01", "resource02"), True)
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_resources, (), False)
-
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_locations, ("location01", "location02"), True)
- self._verifyRecordsCheckEnabled(DirectoryService.recordType_locations, (), False)
-
- def test_normalCacheMiss(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01",))
- self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- fakeODRecord("User 03", guid="D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
- self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
- def test_duplicateRecords(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- fakeODRecord("User 02"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02"))
- self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
- self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
-
-
- def test_duplicateName(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02", guid="A25775BB-1281-4606-98C6-2893B2D5CCD7"),
- fakeODRecord("User 02", guid="30CA2BB9-C935-4A5D-80E2-79266BCB0255"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01",))
- self._verifyDisabledRecords(
- DirectoryService.recordType_users,
- ("user02",),
- ("A25775BB-1281-4606-98C6-2893B2D5CCD7", "30CA2BB9-C935-4A5D-80E2-79266BCB0255"),
- )
-
- def test_duplicateGUID(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
- fakeODRecord("User 03", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01",))
- self._verifyDisabledRecords(
- DirectoryService.recordType_users,
- ("user02", "user03"),
- ("113D7F74-F84A-4F17-8C96-CE8F10D68EF8",),
- )
-
- def test_duplicateCombo(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8"),
- fakeODRecord("User 02", guid="113D7F74-F84A-4F17-8C96-CE8F10D68EF8", shortName="user03"),
- fakeODRecord("User 02", guid="136E369F-DB40-4135-878D-B75D38242D39"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01",))
- self._verifyDisabledRecords(
- DirectoryService.recordType_users,
- ("user02", "user03"),
- ("113D7F74-F84A-4F17-8C96-CE8F10D68EF8", "136E369F-DB40-4135-878D-B75D38242D39"),
- )
-
- def test_duplicateGUIDCacheMiss(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E"),
- fakeODRecord("User 03", guid="D10F3EE0-5014-41D3-8488-3819D3EF3B2A"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01", "user02", "user03"))
- self._verifyDisabledRecords(DirectoryService.recordType_users, (), ())
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E"),
- fakeODRecord("User 02", guid="EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", shortName="user04"),
- fakeODRecord("User 03", guid="62368DDF-0C62-4C97-9A58-DE9FD46131A0"),
- fakeODRecord("User 03", guid="62368DDF-0C62-4C97-9A58-DE9FD46131A0", shortName="user05"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
- self._verifyRecords(DirectoryService.recordType_users, ("user01",))
- self._verifyDisabledRecords(
- DirectoryService.recordType_users,
- ("user02", "user03", "user04", "user05"),
- ("EDB9EE55-31F2-4EA9-B5FB-D8AE2A8BA35E", "62368DDF-0C62-4C97-9A58-DE9FD46131A0"),
- )
-
- def test_groupmembers(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- ],
- DirectoryService.recordType_groups: [
- fakeODRecord("Group 01", members=[
- guidForShortName("user01"),
- guidForShortName("user02"),
- ]),
- fakeODRecord("Group 02", members=[
- guidForShortName("resource01"),
- guidForShortName("user02"),
- ]),
- ],
- DirectoryService.recordType_resources: [
- fakeODRecord("Resource 01"),
- fakeODRecord("Resource 02"),
- ],
- DirectoryService.recordType_locations: [
- fakeODRecord("Location 01"),
- fakeODRecord("Location 02"),
- ],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
- self._service.reloadCache(DirectoryService.recordType_groups)
- self._service.reloadCache(DirectoryService.recordType_resources)
- self._service.reloadCache(DirectoryService.recordType_locations)
-
- group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
- self.assertTrue(group1 is not None)
-
- group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
- self.assertTrue(group2 is not None)
-
- user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
- self.assertTrue(user1 is not None)
- self.assertEqual(set((group1,)), user1.groups())
-
- user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
- self.assertTrue(user2 is not None)
- self.assertEqual(set((group1, group2)), user2.groups())
-
- self._service.fakerecords[DirectoryService.recordType_groups] = [
- fakeODRecord("Group 01", members=[
- guidForShortName("user01"),
- ]),
- fakeODRecord("Group 02", members=[
- guidForShortName("resource01"),
- guidForShortName("user02"),
- ]),
- ]
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_groups, forceUpdate=True)
-
- group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
- self.assertTrue(group1 is not None)
-
- group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
- self.assertTrue(group2 is not None)
-
- user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
- self.assertTrue(user1 is not None)
- self.assertEqual(set((group1,)), user1.groups())
-
- user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
- self.assertTrue(user2 is not None)
- self.assertEqual(set((group2,)), user2.groups())
-
- self._service.fakerecords[DirectoryService.recordType_groups] = [
- fakeODRecord("Group 01", members=[
- guidForShortName("user01"),
- ]),
- fakeODRecord("Group 02", members=[
- guidForShortName("resource01"),
- guidForShortName("user02"),
- ]),
- fakeODRecord("Group 03", members=[
- guidForShortName("user01"),
- guidForShortName("user02"),
- ]),
- ]
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_groups, forceUpdate=True)
-
- group1 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group01")
- self.assertTrue(group1 is not None)
-
- group2 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group02")
- self.assertTrue(group2 is not None)
-
- group3 = self._service.recordWithShortName(DirectoryService.recordType_groups, "group03")
- self.assertTrue(group2 is not None)
-
- user1 = self._service.recordWithShortName(DirectoryService.recordType_users, "user01")
- self.assertTrue(user1 is not None)
- self.assertEqual(set((group1, group3)), user1.groups())
-
- user2 = self._service.recordWithShortName(DirectoryService.recordType_users, "user02")
- self.assertTrue(user2 is not None)
- self.assertEqual(set((group2, group3)), user2.groups())
-
- def test_calendaruseraddress(self):
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- ],
- DirectoryService.recordType_groups: [],
- DirectoryService.recordType_resources: [],
- DirectoryService.recordType_locations: [],
- }
- self._service.refresh(loop=False, master=True)
-
- self._service.reloadCache(DirectoryService.recordType_users)
-
- user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
- self.assertTrue(user1 is not None)
-
- user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
- self.assertTrue(user3 is None)
-
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- fakeODRecord("User 03"),
- ],
- DirectoryService.recordType_groups: [],
- DirectoryService.recordType_resources: [],
- DirectoryService.recordType_locations: [],
- }
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
- user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
- self.assertTrue(user1 is not None)
-
- user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
- self.assertTrue(user3 is not None)
-
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 02"),
- fakeODRecord("User 03"),
- ],
- DirectoryService.recordType_groups: [],
- DirectoryService.recordType_resources: [],
- DirectoryService.recordType_locations: [],
- }
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
-
- user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
- self.assertTrue(user1 is None)
-
- user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
- self.assertTrue(user3 is not None)
-
-
- def test_substantialDecline(self):
- """
- Test the "substantial decline" protection logic in the case where an
- od query returns less than half the results of the previous
- successful one.
- """
-
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- fakeODRecord("User 03"),
- fakeODRecord("User 04"),
- fakeODRecord("User 05"),
- fakeODRecord("User 06"),
- ],
- DirectoryService.recordType_groups: [],
- DirectoryService.recordType_resources: [],
- DirectoryService.recordType_locations: [],
- }
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
- user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
- self.assertTrue(user1 is not None)
-
- # Pretend OD suddenly returned less than half:
- self._service.fakerecords[DirectoryService.recordType_users] = [
- fakeODRecord("User 01"),
- fakeODRecord("User 02"),
- ]
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_users, forceUpdate=True)
- user3 = self._service.recordWithCalendarUserAddress("mailto:user03 at example.com")
- self.assertTrue(user3 is not None)
-
- class AuthCacheTests(TestCase):
-
- def _authenticateUserBasic(self, ignore, node, name, password):
- self._odAccessed = True
- return self._passwords.get(name, "") == password
-
- def setUp(self):
- super(AuthCacheTests, self).setUp()
- self._service = OpenDirectoryService(node="/Search", dosetup=False)
- self._service.servicetags.add("FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar")
- self._passwords = { }
-
- # Monkeypatch the real opendirectory.authenticateUserBasic
- self._prev = opendirectory.authenticateUserBasic
- opendirectory.authenticateUserBasic = self._authenticateUserBasic
-
- def tearDown(self):
- for call in self._service._delayedCalls:
- call.cancel()
- opendirectory.authenticateUserBasic = self._prev
-
- def test_caching(self):
-
- self._service.fakerecords = {
- DirectoryService.recordType_users: [
- fakeODRecord("User 01"),
- ],
- }
- self._service.refresh(loop=False, master=True)
- self._service.reloadCache(DirectoryService.recordType_users)
-
- user1 = self._service.recordWithCalendarUserAddress("mailto:user01 at example.com")
- self._passwords["user01"] = "user01"
- cred = UsernamePassword("user01", "user01")
-
- # Verify we go to OD when password is neither local nor in memcache
- self._odAccessed = False
- self.assertTrue(user1.verifyCredentials(cred))
- self.assertTrue(self._odAccessed)
-
- # Verify we don't go to OD when password is local
- self._odAccessed = False
- self.assertTrue(user1.verifyCredentials(cred))
- self.assertFalse(self._odAccessed)
-
- # Verify we don't go to OD when password not local but *is* in
- # memcache
- del user1.password
- self._odAccessed = False
- self.assertTrue(user1.verifyCredentials(cred))
- self.assertFalse(self._odAccessed)
-
- # Verify a password change will send us to OD
- self._passwords["user01"] = "new user01"
- cred = UsernamePassword("user01", "new user01")
- self._odAccessed = False
- self.assertTrue(user1.verifyCredentials(cred))
- self.assertTrue(self._odAccessed)
-
- # Verify the new password was memcached correctly
- del user1.password
- self._odAccessed = False
- self.assertTrue(user1.verifyCredentials(cred))
- self.assertFalse(self._odAccessed)
-
-
-def fakeODRecord(fullName, shortName=None, guid=None, email=None, addLocator=True, members=None):
- if shortName is None:
- shortName = shortNameForFullName(fullName)
-
- if guid is None:
- guid = guidForShortName(shortName)
- else:
- guid = guid.lower()
-
- if email is None:
- email = "%s at example.com" % (shortName,)
-
- attrs = {
- dsattributes.kDS1AttrDistinguishedName: fullName,
- dsattributes.kDS1AttrGeneratedUID: guid,
- dsattributes.kDSNAttrEMailAddress: email,
- dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1",
- }
-
- if members:
- attrs[dsattributes.kDSNAttrGroupMembers] = members
-
- if addLocator:
- attrs[dsattributes.kDSNAttrServicesLocator] = "FE588D50-0514-4DF9-BCB5-8ECA5F3DA274:030572AE-ABEC-4E0F-83C9-FCA304769E5F:calendar"
-
- return [ shortName, attrs ]
-
-def shortNameForFullName(fullName):
- return fullName.lower().replace(" ", "")
-
-def guidForShortName(shortName):
- return uuidFromName(OpenDirectoryService.baseGUID, shortName)
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryschema.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryschema.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_opendirectoryschema.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,1298 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-try:
- from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
- from twistedcaldav.directory.appleopendirectory import OpenDirectoryInitError
- import dsattributes
-except ImportError:
- pass
-else:
- from twistedcaldav.directory.directory import DirectoryService
- import twisted.trial.unittest
-
- class PlistParse (twisted.trial.unittest.TestCase):
- """
- Test Open Directory service schema.
- """
-
- plist_nomacosxserver_key = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
- </dict>
-</plist>
-"""
-
- plist_nocalendarservice = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_noserviceinfo = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
- <dict>
- <key>hostname</key>
- <string>calendar.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>8008</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>8443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_disabledservice = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <string>443</string>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
- <dict>
- <key>hostname</key>
- <string>calendar.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>8008</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>8443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>calendar</key>
- <dict>
- <key>enabled</key>
- <false/>
- <key>templates</key>
- <dict>
- <key>principalPath</key>
- <string>/principals/%(type)s/%(name)s</string>
- <key>calendarUserAddresses</key>
- <array>
- <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
- <string>mailto:%(email)s</string>
- <string>urn:uuid:%(guid)s</string>
- </array>
- </dict>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_nohostname = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <string>443</string>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
- <dict>
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>8008</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>8443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>calendar</key>
- <dict>
- <key>templates</key>
- <dict>
- <key>principalPath</key>
- <string>/principals/%(type)s/%(name)s</string>
- <key>calendarUserAddresses</key>
- <array>
- <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
- <string>mailto:%(email)s</string>
- <string>urn:uuid:%(guid)s</string>
- </array>
- </dict>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_nohostdetails = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <string>443</string>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
- <dict>
- <key>hostname</key>
- <string>calendar.apple.com</string>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>calendar</key>
- <dict>
- <key>templates</key>
- <dict>
- <key>principalPath</key>
- <string>/principals/%(type)s/%(name)s</string>
- <key>calendarUserAddresses</key>
- <array>
- <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
- <string>mailto:%(email)s</string>
- <string>urn:uuid:%(guid)s</string>
- </array>
- </dict>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_good = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <string>443</string>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
- <dict>
- <key>hostname</key>
- <string>calendar.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>8008</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>8443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>calendar</key>
- <dict>
- <key>templates</key>
- <dict>
- <key>principalPath</key>
- <string>/principals/%(type)s/%(name)s</string>
- <key>calendarUserAddresses</key>
- <array>
- <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
- <string>mailto:%(email)s</string>
- <string>urn:uuid:%(guid)s</string>
- </array>
- </dict>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_good_other = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>4F088107-51FD-4DE5-904D-2C0AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <string>443</string>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>C18C34AC-3D9E-403C-8A33-BFC303F3840E</key>
- <dict>
- <key>hostname</key>
- <string>privatecalendar.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>8008</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>8443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>calendar</key>
- <dict>
- <key>templates</key>
- <dict>
- <key>principalPath</key>
- <string>/principals/%(type)s/%(name)s</string>
- <key>calendarUserAddresses</key>
- <array>
- <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
- <string>mailto:%(email)s</string>
- <string>urn:uuid:%(guid)s</string>
- </array>
- </dict>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- plist_duplicate = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
- <dict>
- <key>ReplicaName</key>
- <string>Master</string>
-
- <key>com.apple.od.role</key>
- <string>master</string>
-
- <key>com.apple.macosxserver.virtualhosts</key>
- <dict>
- <key>F4088107-51FD-4DE5-904D-C20AD9C6C893</key>
- <dict>
- <key>hostname</key>
- <string>foo.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>80</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <string>443</string>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>wiki</string>
- <string>webCalendar</string>
- <string>webMailingList</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>webCalendar</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/webcalendar</string>
- </dict>
- <key>wiki</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/wiki</string>
- </dict>
- <key>webMailingList</key>
- <dict>
- <key>enabled</key>
- <true/>
- <key>urlMask</key>
- <string>%(scheme)s://%(hostname)s:%(port)s/groups/%(name)s/mailinglist</string>
- </dict>
- </dict>
- </dict>
-
- <key>1C8C34AC-3D9E-403C-8A33-FBC303F3840E</key>
- <dict>
- <key>hostname</key>
- <string>calendar.apple.com</string>
-
- <key>hostDetails</key>
- <dict>
- <key>access</key>
- <dict>
- <key>somethingorother</key>
- <string>somethingelse</string>
- </dict>
- <key>http</key>
- <dict>
- <key>port</key>
- <integer>8008</integer>
- </dict>
- <key>https</key>
- <dict>
- <key>port</key>
- <integer>8443</integer>
- </dict>
- </dict>
-
- <key>serviceType</key>
- <array>
- <string>calendar</string>
- </array>
-
- <key>serviceInfo</key>
- <dict>
- <key>calendar</key>
- <dict>
- <key>templates</key>
- <dict>
- <key>principalPath</key>
- <string>/principals/%(type)s/%(name)s</string>
- <key>calendarUserAddresses</key>
- <array>
- <string>%(scheme)s://%(hostname)s:%(port)s/principals/%(type)s/%(name)s</string>
- <string>mailto:%(email)s</string>
- <string>urn:uuid:%(guid)s</string>
- </array>
- </dict>
- </dict>
- </dict>
- </dict>
-
- </dict>
- </dict>
-</plist>
-"""
-
- def test_plist_errors(self):
- def _doParse(plist, title):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- if service._parseServiceInfo("calendar.apple.com", "recordit", {
- 'dsAttrTypeNative:apple-serviceinfo' : plist,
- dsattributes.kDS1AttrGeneratedUID: "GUIDIFY",
- dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1"}) and service.servicetags:
- self.fail(msg="Plist parse should have failed: %s" % (title,))
-
- plists = (
- (PlistParse.plist_nomacosxserver_key, "nomacosxserver_key"),
- (PlistParse.plist_nocalendarservice, "nocalendarservice"),
- (PlistParse.plist_noserviceinfo, "noserviceinfo"),
- (PlistParse.plist_disabledservice, "disabledservice"),
- (PlistParse.plist_nohostname, "nohostname"),
- (PlistParse.plist_nohostdetails, "nohostdetails"),
- )
- for plist, title in plists:
- _doParse(plist, title)
-
- def test_goodplist(self):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- if not service._parseServiceInfo("calendar.apple.com", "recordit", {
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_good,
- dsattributes.kDS1AttrGeneratedUID: "GUIDIFY",
- dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1"}):
- self.fail(msg="Plist parse should not have failed")
- else:
- # Verify that we extracted the proper items
- self.assertEqual(service.servicetags.pop(), "GUIDIFY:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")
-
- def test_expandcuaddrs(self):
- def doTest(recordName, record, result, title):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- if not service._parseServiceInfo("calendar.apple.com", recordName, {
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_good,
- dsattributes.kDS1AttrGeneratedUID: "GUIDIFY",
- dsattributes.kDSNAttrMetaNodeLocation: "/LDAPv3/127.0.0.1"}):
- self.fail(msg="Plist parse should not have failed: %s" % (recordName,))
- else:
- expanded = service._calendarUserAddresses(DirectoryService.recordType_users, recordName, record)
-
- # Verify that we extracted the proper items
- self.assertEqual(expanded, result, msg=title % (expanded, result,))
-
- data = (
- (
- "user01",
- {
- dsattributes.kDS1AttrGeneratedUID: "GUID-USER-01",
- dsattributes.kDSNAttrEMailAddress: "user01 at example.com",
- },
- set((
- "mailto:user01 at example.com",
- )),
- "User with one email address, %s != %s",
- ),
- (
- "user02",
- {
- dsattributes.kDS1AttrGeneratedUID: "GUID-USER-02",
- dsattributes.kDSNAttrEMailAddress: ["user02 at example.com", "user02 at calendar.example.com"],
- },
- set((
- "mailto:user02 at example.com",
- "mailto:user02 at calendar.example.com",
- )),
- "User with multiple email addresses, %s != %s",
- ),
- (
- "user03",
- {
- dsattributes.kDS1AttrGeneratedUID: "GUID-USER-03",
- },
- set(()),
- "User with no email addresses, %s != %s",
- ),
- )
-
- for recordName, record, result, title in data:
- doTest(recordName, record, result, title)
-
- class ODRecordsParse (twisted.trial.unittest.TestCase):
-
- record_localod_good = ("computer1.apple.com", {
- dsattributes.kDS1AttrGeneratedUID : "GUID1",
- dsattributes.kDSNAttrRecordName : "computer1.apple.com",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_good,
- dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
- })
- record_localod_good_other = ("computer2.apple.com", {
- dsattributes.kDS1AttrGeneratedUID : "GUID1",
- dsattributes.kDSNAttrRecordName : "computer2.apple.com",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_good_other,
- dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
- })
- record_localod_duplicate = ("computer1", {
- dsattributes.kDS1AttrGeneratedUID : "GUID1_bad",
- dsattributes.kDSNAttrRecordName : "computer1",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_duplicate,
- dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
- })
- record_remoteod_good = ("computer3.apple.com", {
- dsattributes.kDS1AttrGeneratedUID : "GUID2",
- dsattributes.kDSNAttrRecordName : "computer3.apple.com",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_good,
- dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/directory.apple.com",
- })
- record_remoteod_duplicate = ("computer3", {
- dsattributes.kDS1AttrGeneratedUID : "GUID2",
- dsattributes.kDSNAttrRecordName : "computer3",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_duplicate,
- dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/directory.apple.com",
- })
- record_default_good = ("computer4.apple.com", {
- dsattributes.kDS1AttrGeneratedUID : "GUID3",
- dsattributes.kDSNAttrRecordName : "computer4.apple.com",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_good,
- dsattributes.kDSNAttrMetaNodeLocation : "/Local/Default",
- })
- record_default_duplicate = ("computer4", {
- dsattributes.kDS1AttrGeneratedUID : "GUID3",
- dsattributes.kDSNAttrRecordName : "computer4",
- 'dsAttrTypeNative:apple-serviceinfo' : PlistParse.plist_duplicate,
- dsattributes.kDSNAttrMetaNodeLocation : "/Local/Default",
- })
-
- def test_odrecords_error(self):
- def _doParseRecords(recordlist, title):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- try:
- service._parseComputersRecords(recordlist, "calendar.apple.com")
- self.fail(msg="Record parse should have failed: %s" % (title,))
- except OpenDirectoryInitError:
- pass
-
- records = (
- ((), "no records found"),
- ((
- (ODRecordsParse.record_localod_good_other[0], ODRecordsParse.record_localod_good_other[1]),
- ), "non-matching record found"),
- )
-
- for recordlist, title in records:
- _doParseRecords(recordlist, title)
-
- def test_odrecords_good(self):
- def _doParseRecords(recordlist, title):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- try:
- service._parseComputersRecords(recordlist, "calendar.apple.com")
- except OpenDirectoryInitError, ex:
- self.fail(msg="Record parse should not have failed: \"%s\" with error: %s" % (title, ex))
-
- records = (
- ((
- (ODRecordsParse.record_localod_good[0], ODRecordsParse.record_localod_good[1]),
- ), "single good plist"),
- ((
- (ODRecordsParse.record_localod_good[0], ODRecordsParse.record_localod_good[1]),
- (ODRecordsParse.record_localod_good_other[0], ODRecordsParse.record_localod_good_other[1]),
- ), "multiple plists"),
- )
-
- for recordlist, title in records:
- _doParseRecords(recordlist, title)
-
- def test_odrecords_multiple(self):
- def _doParseRecords(recordlist, title, tags):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- service._parseComputersRecords(recordlist, "calendar.apple.com")
-
- self.assertEquals(service.servicetags, set(tags),
- "Got wrong service tags: %s and %s" % (service.servicetags, set(tags),))
-
- records = (
- (((ODRecordsParse.record_remoteod_good[0], ODRecordsParse.record_remoteod_good[1]),
- (ODRecordsParse.record_localod_good[0], ODRecordsParse.record_localod_good[1]),
- (ODRecordsParse.record_default_good[0], ODRecordsParse.record_default_good[1])),
- "Three records",
- ("GUID2:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
- (((ODRecordsParse.record_localod_good[0], ODRecordsParse.record_localod_good[1]),
- (ODRecordsParse.record_default_good[0], ODRecordsParse.record_default_good[1])),
- "Two records",
- ("GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
- (((ODRecordsParse.record_default_good[0], ODRecordsParse.record_default_good[1]),),
- "One record",
- ("GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",)),
- )
-
- for recordlist, title, tags in records:
- _doParseRecords(recordlist, title, tags)
-
- def test_odrecords_duplicates(self):
- def _doParseRecords(recordlist, title, items, tags):
- service = OpenDirectoryService(node="/Search", dosetup=False)
- service._parseComputersRecords(recordlist, "calendar.apple.com")
- self.assertEquals(service.servicetags, set(tags))
-
- records = (
- (((ODRecordsParse.record_remoteod_good[0], ODRecordsParse.record_remoteod_good[1]),
- (ODRecordsParse.record_remoteod_duplicate[0], ODRecordsParse.record_remoteod_duplicate[1]),
- (ODRecordsParse.record_localod_good[0], ODRecordsParse.record_localod_good[1]),
- (ODRecordsParse.record_default_good[0], ODRecordsParse.record_default_good[1])),
- "Remote Record Duplicated", ("computer3.apple.com", "computer3",),
- ("GUID2:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID2:1C8C34AC-3D9E-403C-8A33-FBC303F3840E:calendar",
- "GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
- (((ODRecordsParse.record_localod_good[0], ODRecordsParse.record_localod_good[1]),
- (ODRecordsParse.record_localod_duplicate[0], ODRecordsParse.record_localod_duplicate[1]),
- (ODRecordsParse.record_default_good[0], ODRecordsParse.record_default_good[1])),
- "Local OD Duplicated", ("computer1.apple.com", "computer1",),
- ("GUID1:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID1_bad:1C8C34AC-3D9E-403C-8A33-FBC303F3840E:calendar",
- "GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar")),
- (((ODRecordsParse.record_default_good[0], ODRecordsParse.record_default_good[1]),
- (ODRecordsParse.record_default_duplicate[0], ODRecordsParse.record_default_duplicate[1])),
- "Local Node Duplicated", ("computer4.apple.com", "computer4",),
- ("GUID3:C18C34AC-3D9E-403C-8A33-BFC303F3840E:calendar",
- "GUID3:1C8C34AC-3D9E-403C-8A33-FBC303F3840E:calendar")),
- )
-
- for recordlist, title, items, tags in records:
- _doParseRecords(recordlist, title, items, tags)
-
- class ODResourceInfoParse (twisted.trial.unittest.TestCase):
-
- plist_good_false = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.WhitePagesFramework</key>
- <dict>
- <key>AutoAcceptsInvitation</key>
- <false/>
- <key>Label</key>
- <string>Location</string>
- <key>CalendaringDelegate</key>
- <string>1234-GUID-5678</string>
- <key>ReadOnlyCalendaringDelegate</key>
- <string>1234-GUID-5679</string>
- </dict>
-</dict>
-</plist>
-"""
-
- plist_good_true = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.WhitePagesFramework</key>
- <dict>
- <key>AutoAcceptsInvitation</key>
- <true/>
- <key>Label</key>
- <string>Location</string>
- <key>CalendaringDelegate</key>
- <string></string>
- <key>ReadOnlyCalendaringDelegate</key>
- <string></string>
- </dict>
-</dict>
-</plist>
-"""
-
- plist_good_missing = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.WhitePagesFramework</key>
- <dict>
- <key>Label</key>
- <string>Location</string>
- </dict>
-</dict>
-</plist>
-"""
-
- plist_bad = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.WhitePagesFramework</key>
- <string>bogus</string>
-</dict>
-</plist>
-"""
-
- plist_wrong = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.YellowPagesFramework</key>
- <dict>
- <key>AutoAcceptsInvitation</key>
- <true/>
- <key>Label</key>
- <string>Location</string>
- <key>CalendaringDelegate</key>
- <string>1234-GUID-5678</string>
- <key>ReadOnlyCalendaringDelegate</key>
- <string>1234-GUID-5679</string>
- </dict>
-</dict>
-</plist>
-"""
-
- plist_invalid = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.WhitePagesFramework</key>
- <string>bogus</string>
- <string>another bogon</string>
-</dict>
-</plist>
-"""
-
- plist_invalid_xml = """<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
- <key>com.apple.WhitePagesFramework</key>
- <string>R&D</string>
-</dict>
-</plist>
-"""
-
- test_bool = (
- (plist_good_false, False, "1234-GUID-5678", "1234-GUID-5679", None),
- (plist_good_true, True, "", "", None),
- (plist_good_missing, False, None, None, None),
- (plist_wrong, False, None, None, None),
- (plist_bad, False, None, None, ValueError),
- (plist_invalid, False, None, None, ValueError),
- (plist_invalid_xml, False, None, None, ValueError),
- )
-
- def test_plists(self):
- service = OpenDirectoryService(node="/Search", dosetup=False)
-
- for item in ODResourceInfoParse.test_bool:
- if item[4] is None:
- item1, item2, item3 = service._parseResourceInfo(item[0], "guid", "locations", "name")
- self.assertEqual(item1, item[1])
- self.assertEqual(item2, item[2])
- self.assertEqual(item3, item[3])
- else:
- self.assertRaises(item[4], service._parseResourceInfo, item[0], "guid", "locations", "name")
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_principal.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_principal.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -16,19 +16,19 @@
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
from twisted.web2.dav.resource import AccessDeniedError
from twisted.web2.test.test_server import SimpleRequest
-from twisted.web2.dav.test.util import serialize
from twistedcaldav.static import CalendarHomeProvisioningFile
-from twistedcaldav.directory.apache import BasicDirectoryService, DigestDirectoryService
+from twistedcaldav.config import config
+from twistedcaldav.directory import augment, calendaruserproxy
from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.test.test_apache import basicUserFile, digestUserFile, groupFile, digestRealm
from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
from twistedcaldav.directory.principal import DirectoryPrincipalTypeProvisioningResource
from twistedcaldav.directory.principal import DirectoryPrincipalResource
@@ -38,11 +38,6 @@
import twistedcaldav.test.util
-directoryServices = (
- BasicDirectoryService(digestRealm, basicUserFile, groupFile),
- DigestDirectoryService(digestRealm, digestUserFile, groupFile),
- XMLDirectoryService(xmlFile),
-)
class ProvisionedPrincipals (twistedcaldav.test.util.TestCase):
"""
@@ -51,9 +46,13 @@
def setUp(self):
super(ProvisionedPrincipals, self).setUp()
+ self.directoryServices = (
+ XMLDirectoryService(xmlFile=xmlFile),
+ )
+
# Set up a principals hierarchy for each service we're testing with
self.principalRootResources = {}
- for directory in directoryServices:
+ for directory in self.directoryServices:
name = directory.__class__.__name__
url = "/" + name + "/"
@@ -63,6 +62,9 @@
self.principalRootResources[directory.__class__.__name__] = provisioningResource
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+ calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(self.mktemp())
+
def test_hierarchy(self):
"""
DirectoryPrincipalProvisioningResource.listChildren(),
@@ -77,7 +79,7 @@
DirectoryPrincipalResource.principalURL(),
"""
- for directory in directoryServices:
+ for directory in self.directoryServices:
#print "\n -> %s" % (directory.__class__.__name__,)
provisioningResource = self.principalRootResources[directory.__class__.__name__]
@@ -139,7 +141,7 @@
"""
DirectoryPrincipalProvisioningResource.principalForUser()
"""
- for directory in directoryServices:
+ for directory in self.directoryServices:
provisioningResource = self.principalRootResources[directory.__class__.__name__]
for user in directory.listRecords(DirectoryService.recordType_users):
@@ -162,7 +164,7 @@
"""
for provisioningResource, recordType, recordResource, record in self._allRecords():
principal = provisioningResource.principalForRecord(record)
- self.failIf(principal is None)
+ self.failIf(principal is None, msg=str(record))
self.assertEquals(record, principal.record)
def test_principalForCalendarUserAddress(self):
@@ -184,6 +186,13 @@
else:
self.failIf(principal is not None)
+ # Explicitly check the disabled record
+ provisioningResource = self.principalRootResources['XMLDirectoryService']
+ self.failIf(provisioningResource.principalForCalendarUserAddress("mailto:nocalendar at example.com") is not None)
+ self.failIf(provisioningResource.principalForCalendarUserAddress("urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5") is not None)
+ self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/users/nocalendar/") is not None)
+ self.failIf(provisioningResource.principalForCalendarUserAddress("/principals/__uids__/543D28BA-F74F-4D5F-9243-B3E3A61171E5/") is not None)
+
def test_autoSchedule(self):
"""
DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
@@ -251,24 +260,6 @@
memberships = yield recordResource.groupMemberships()
self.failUnless(set(record.groups()).issubset(set(r.record for r in memberships if hasattr(r, "record"))))
- def test_proxies(self):
- """
- DirectoryPrincipalResource.proxies()
- """
- for provisioningResource, recordType, recordResource, record in self._allRecords():
- if record.enabledForCalendaring:
- self.failUnless(set(record.proxies()).issubset(set(r.record for r in recordResource.proxies())))
- self.assertEqual(record.hasEditableProxyMembership(), recordResource.hasEditableProxyMembership())
-
- def test_read_only_proxies(self):
- """
- DirectoryPrincipalResource.proxies()
- """
- for provisioningResource, recordType, recordResource, record in self._allRecords():
- if record.enabledForCalendaring:
- self.failUnless(set(record.readOnlyProxies()).issubset(set(r.record for r in recordResource.readOnlyProxies())))
- self.assertEqual(record.hasEditableProxyMembership(), recordResource.hasEditableProxyMembership())
-
def test_principalUID(self):
"""
DirectoryPrincipalResource.principalUID()
@@ -305,7 +296,7 @@
# Need to create a calendar home provisioner for each service.
calendarRootResources = {}
- for directory in directoryServices:
+ for directory in self.directoryServices:
url = "/homes_" + directory.__class__.__name__ + "/"
path = os.path.join(self.docroot, url[1:])
@@ -352,37 +343,29 @@
"""
Default access controls for principals.
"""
- def work():
- for provisioningResource, recordType, recordResource, record in self._allRecords():
- for args in _authReadOnlyPrivileges(recordResource, recordResource.principalURL()):
- yield args
+ for provisioningResource, recordType, recordResource, record in self._allRecords():
+ for args in _authReadOnlyPrivileges(self, recordResource, recordResource.principalURL()):
+ yield self._checkPrivileges(*args)
- for args in work():
- yield self._checkPrivileges(*args)
-
@inlineCallbacks
def test_defaultAccessControlList_provisioners(self):
"""
Default access controls for principal provisioning resources.
"""
- def work():
- for directory in directoryServices:
- #print "\n -> %s" % (directory.__class__.__name__,)
- provisioningResource = self.principalRootResources[directory.__class__.__name__]
+ for directory in self.directoryServices:
+ #print "\n -> %s" % (directory.__class__.__name__,)
+ provisioningResource = self.principalRootResources[directory.__class__.__name__]
- for args in _authReadOnlyPrivileges(provisioningResource, provisioningResource.principalCollectionURL()):
- yield args
+ for args in _authReadOnlyPrivileges(self, provisioningResource, provisioningResource.principalCollectionURL()):
+ yield self._checkPrivileges(*args)
- for recordType in provisioningResource.listChildren():
- #print " -> %s" % (recordType,)
- typeResource = provisioningResource.getChild(recordType)
+ for recordType in provisioningResource.listChildren():
+ #print " -> %s" % (recordType,)
+ typeResource = provisioningResource.getChild(recordType)
- for args in _authReadOnlyPrivileges(typeResource, typeResource.principalCollectionURL()):
- yield args
+ for args in _authReadOnlyPrivileges(self, typeResource, typeResource.principalCollectionURL()):
+ yield self._checkPrivileges(*args)
- for args in work():
- yield self._checkPrivileges(*args)
-
def _allRecords(self):
"""
@return: an iterable of tuples
@@ -393,7 +376,7 @@
C{record} is the directory service record
for each record in each directory in C{directoryServices}.
"""
- for directory in directoryServices:
+ for directory in self.directoryServices:
provisioningResource = self.principalRootResources[directory.__class__.__name__]
for recordType in directory.recordTypes():
for record in directory.listRecords(recordType):
@@ -425,13 +408,17 @@
d.addCallback(gotResource)
return d
-def _authReadOnlyPrivileges(resource, url):
- for principal, privilege, allowed in (
- ( davxml.All() , davxml.Read() , False ),
- ( davxml.All() , davxml.Write() , False ),
- ( davxml.Unauthenticated() , davxml.Read() , False ),
- ( davxml.Unauthenticated() , davxml.Write() , False ),
- ( davxml.Authenticated() , davxml.Read() , True ),
- ( davxml.Authenticated() , davxml.Write() , False ),
- ):
+def _authReadOnlyPrivileges(self, resource, url):
+ items = []
+ for provisioningResource, recordType, recordResource, record in self._allRecords():
+ if recordResource == resource:
+ items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Read() , True ))
+ items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Write() , True ))
+ else:
+ items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Read() , True ))
+ items.append(( davxml.HRef().fromString(recordResource.principalURL()), davxml.Write() , False ))
+ items.append(( davxml.Unauthenticated() , davxml.Read() , False ))
+ items.append(( davxml.Unauthenticated() , davxml.Write() , False ))
+
+ for principal, privilege, allowed in items:
yield resource, url, principal, privilege, allowed
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipaldb.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipaldb.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipaldb.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -15,18 +15,19 @@
##
from twistedcaldav.config import config
-import os
-
from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.directory.calendaruserproxy import CalendarUserProxyDatabase
+from twistedcaldav.directory.calendaruserproxy import ProxySqliteDB,\
+ ProxyPostgreSQLDB
import twistedcaldav.test.util
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+from twistedcaldav.directory import calendaruserproxy
-class ProxyPrincipalDB (twistedcaldav.test.util.TestCase):
+class ProxyPrincipalDBSqlite (twistedcaldav.test.util.TestCase):
"""
Directory service provisioned principals.
"""
- class old_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+ class old_ProxyDB(ProxySqliteDB):
def _db_version(self):
"""
@@ -34,7 +35,7 @@
"""
return "3"
- def _db_init_data_tables(self, q):
+ def _db_init_data_tables(self):
"""
Initialise the underlying database tables.
@param q: a database cursor to use.
@@ -43,7 +44,7 @@
#
# GROUPS table
#
- q.execute(
+ return self.execute(
"""
create table GROUPS (
GROUPNAME text,
@@ -52,7 +53,7 @@
"""
)
- class new_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+ class new_ProxyDB(ProxySqliteDB):
def _db_version(self):
"""
@@ -60,7 +61,7 @@
"""
return "11"
- class newer_CalendarUserProxyDatabase(CalendarUserProxyDatabase):
+ class newer_ProxyDB(ProxySqliteDB):
def _db_version(self):
"""
@@ -73,8 +74,7 @@
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = CalendarUserProxyDatabase(db_path)
+ db = ProxySqliteDB(db_path)
yield db.setGroupMembers("A", ("B", "C", "D",))
membersA = yield db.getMembers("A")
@@ -83,29 +83,28 @@
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
+ @inlineCallbacks
def test_DBIndexed(self):
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = CalendarUserProxyDatabase(db_path)
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
+ db = ProxySqliteDB(db_path)
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+ @inlineCallbacks
def test_OldDB(self):
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = self.old_CalendarUserProxyDatabase(db_path)
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
+ db = self.old_ProxyDB(db_path)
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
@inlineCallbacks
def test_DBUpgrade(self):
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = self.old_CalendarUserProxyDatabase(db_path)
+ db = self.old_ProxyDB(db_path)
yield db.setGroupMembers("A", ("B", "C", "D",))
membersA = yield db.getMembers("A")
@@ -113,19 +112,19 @@
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
- db._db_close()
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
+ db.close()
db = None
- db = CalendarUserProxyDatabase(db_path)
+ db = ProxySqliteDB(db_path)
membersA = yield db.getMembers("A")
membershipsB = yield db.getMemberships("B")
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
- db._db_close()
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+ db.close()
db = None
@inlineCallbacks
@@ -133,8 +132,7 @@
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = self.old_CalendarUserProxyDatabase(db_path)
+ db = self.old_ProxyDB(db_path)
yield db.setGroupMembers("A", ("B", "C", "D",))
membersA = yield db.getMembers("A")
@@ -142,19 +140,19 @@
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set())
- db._db_close()
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set())
+ db.close()
db = None
- db = self.new_CalendarUserProxyDatabase(db_path)
+ db = self.new_ProxyDB(db_path)
membersA = yield db.getMembers("A")
membershipsB = yield db.getMemberships("B")
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
- db._db_close()
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+ db.close()
db = None
@inlineCallbacks
@@ -162,8 +160,7 @@
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = self.new_CalendarUserProxyDatabase(db_path)
+ db = self.new_ProxyDB(db_path)
yield db.setGroupMembers("A", ("B", "C", "D",))
membersA = yield db.getMembers("A")
@@ -171,31 +168,30 @@
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
- db._db_close()
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+ db.close()
db = None
- db = self.newer_CalendarUserProxyDatabase(db_path)
+ db = self.newer_ProxyDB(db_path)
membersA = yield db.getMembers("A")
membershipsB = yield db.getMemberships("B")
self.assertEqual(membersA, set(("B", "C", "D",)))
self.assertEqual(membershipsB, set(("A",)))
- self.assertEqual(set([row[1] for row in db._db_execute("PRAGMA index_list(GROUPS)")]), set(("GROUPNAMES", "MEMBERS")))
- db._db_close()
+ self.assertEqual(set([row[1] for row in (yield db.query("PRAGMA index_list(GROUPS)"))]), set(("GROUPNAMES", "MEMBERS")))
+ db.close()
db = None
@inlineCallbacks
def test_cachingDBInsert(self):
for processType in ("Single", "Combined",):
- config.processType = processType
+ config.ProcessType = processType
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = CalendarUserProxyDatabase(db_path)
+ db = ProxySqliteDB(db_path)
# Do one insert and check the result
yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -231,12 +227,11 @@
def test_cachingDBRemove(self):
for processType in ("Single", "Combined",):
- config.processType = processType
+ config.ProcessType = processType
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = CalendarUserProxyDatabase(db_path)
+ db = ProxySqliteDB(db_path)
# Do one insert and check the result
yield db.setGroupMembers("A", ("B", "C", "D",))
@@ -270,15 +265,88 @@
self.assertEqual(membershipsD, set())
@inlineCallbacks
+ def test_cachingDBRemoveSpecial(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ db_path = self.mktemp()
+ db = ProxySqliteDB(db_path)
+
+ # Do one insert and check the result
+ yield db.setGroupMembers("A", ("B", "C", "D",))
+ yield db.setGroupMembers("X", ("B", "C",))
+
+ membershipsB = yield db.getMemberships("B")
+ membershipsC = yield db.getMemberships("C")
+ membershipsD = yield db.getMemberships("D")
+
+ # Remove and check the result
+ yield db.removeGroup("A")
+
+ membersA = yield db.getMembers("A")
+ membersX = yield db.getMembers("X")
+ membershipsB = yield db.getMemberships("B")
+ membershipsC = yield db.getMemberships("C")
+ membershipsD = yield db.getMemberships("D")
+
+ self.assertEqual(membersA, set())
+ self.assertEqual(membersX, set(("B", "C",)))
+ self.assertEqual(membershipsB, set("X",))
+ self.assertEqual(membershipsC, set("X",))
+ self.assertEqual(membershipsD, set())
+
+ @inlineCallbacks
+ def test_cachingDBRemovePrincipal(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ db_path = self.mktemp()
+ db = ProxySqliteDB(db_path)
+
+ # Do one insert and check the result
+ yield db.setGroupMembers("A", ("B", "C", "D",))
+ yield db.setGroupMembers("X", ("B", "C",))
+
+ membersA = yield db.getMembers("A")
+ membersX = yield db.getMembers("X")
+ membershipsB = yield db.getMemberships("B")
+ membershipsC = yield db.getMemberships("C")
+ membershipsD = yield db.getMemberships("D")
+
+ self.assertEqual(membersA, set(("B", "C", "D",)))
+ self.assertEqual(membersX, set(("B", "C",)))
+ self.assertEqual(membershipsB, set(("A", "X",)))
+ self.assertEqual(membershipsC, set(("A", "X",)))
+ self.assertEqual(membershipsD, set(("A",)))
+
+ # Remove and check the result
+ yield db.removePrincipal("B")
+
+ membersA = yield db.getMembers("A")
+ membersX = yield db.getMembers("X")
+ membershipsB = yield db.getMemberships("B")
+ membershipsC = yield db.getMemberships("C")
+ membershipsD = yield db.getMemberships("D")
+
+ self.assertEqual(membersA, set(("C", "D",)))
+ self.assertEqual(membersX, set(("C",)))
+ self.assertEqual(membershipsB, set())
+ self.assertEqual(membershipsC, set(("A", "X",)))
+ self.assertEqual(membershipsD, set(("A",),))
+
+ @inlineCallbacks
def test_cachingDBInsertUncached(self):
for processType in ("Single", "Combined",):
- config.processType = processType
+ config.ProcessType = processType
# Get the DB
db_path = self.mktemp()
- os.mkdir(db_path)
- db = CalendarUserProxyDatabase(db_path)
+ db = ProxySqliteDB(db_path)
# Do one insert and check the result for the one we will remove
yield db.setGroupMembers("AA", ("BB", "CC", "DD",))
@@ -299,3 +367,234 @@
self.assertEqual(membershipsDD, set())
self.assertEqual(membershipsEE, set(("AA",)))
+class ProxyPrincipalDBPostgreSQL (twistedcaldav.test.util.TestCase):
+ """
+ Directory service provisioned principals.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+
+ super(ProxyPrincipalDBPostgreSQL, self).setUp()
+ self.db = ProxyPostgreSQLDB(host="localhost", database="proxies")
+ yield self.db.clean()
+
+ @inlineCallbacks
+ def tearDown(self):
+ yield self.db.close()
+ self.db = None
+
+ @inlineCallbacks
+ def test_normalDB(self):
+
+ # Get the DB
+ yield self.db.clean()
+
+ calendaruserproxy.ProxyDBService = self.db
+ loader = XMLCalendarUserProxyLoader("/Volumes/Data/Users/cyrusdaboo/Documents/Development/Apple/eclipse/CalendarServer-3/conf/auth/proxies-test.xml")
+ yield loader.updateProxyDB()
+
+ yield self.db.setGroupMembers("A", ("B", "C", "D",))
+
+ membersA = yield self.db.getMembers("A")
+ membershipsB = yield self.db.getMemberships("B")
+
+ self.assertEqual(membersA, set(("B", "C", "D",)))
+ self.assertEqual(membershipsB, set(("A",)))
+
+ @inlineCallbacks
+ def test_DBIndexed(self):
+
+ # Get the DB
+ yield self.db.clean()
+ self.assertTrue((yield self.db.queryOne("select hasindexes from pg_tables where tablename = 'groups'")))
+
+ @inlineCallbacks
+ def test_cachingDBInsert(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ yield self.db.clean()
+
+ # Do one insert and check the result
+ yield self.db.setGroupMembers("A", ("B", "C", "D",))
+
+ membersA = yield self.db.getMembers("A")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+ membershipsE = yield self.db.getMemberships("E")
+
+ self.assertEqual(membersA, set(("B", "C", "D",)))
+ self.assertEqual(membershipsB, set(("A",)))
+ self.assertEqual(membershipsC, set(("A",)))
+ self.assertEqual(membershipsD, set(("A",)))
+ self.assertEqual(membershipsE, set(()))
+
+ # Change and check the result
+ yield self.db.setGroupMembers("A", ("B", "C", "E",))
+
+ membersA = yield self.db.getMembers("A")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+ membershipsE = yield self.db.getMemberships("E")
+
+ self.assertEqual(membersA, set(("B", "C", "E",)))
+ self.assertEqual(membershipsB, set(("A",)))
+ self.assertEqual(membershipsC, set(("A",)))
+ self.assertEqual(membershipsD, set())
+ self.assertEqual(membershipsE, set(("A",)))
+
+ @inlineCallbacks
+ def test_cachingDBRemove(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ yield self.db.clean()
+
+ # Do one insert and check the result
+ yield self.db.setGroupMembers("A", ("B", "C", "D",))
+ yield self.db.setGroupMembers("X", ("B", "C",))
+
+ membersA = yield self.db.getMembers("A")
+ membersX = yield self.db.getMembers("X")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+
+ self.assertEqual(membersA, set(("B", "C", "D",)))
+ self.assertEqual(membersX, set(("B", "C",)))
+ self.assertEqual(membershipsB, set(("A", "X",)))
+ self.assertEqual(membershipsC, set(("A", "X",)))
+ self.assertEqual(membershipsD, set(("A",)))
+
+ # Remove and check the result
+ yield self.db.removeGroup("A")
+
+ membersA = yield self.db.getMembers("A")
+ membersX = yield self.db.getMembers("X")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+
+ self.assertEqual(membersA, set())
+ self.assertEqual(membersX, set(("B", "C",)))
+ self.assertEqual(membershipsB, set("X",))
+ self.assertEqual(membershipsC, set("X",))
+ self.assertEqual(membershipsD, set())
+
+ @inlineCallbacks
+ def test_cachingDBRemoveSpecial(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ yield self.db.clean()
+
+ # Do one insert and check the result
+ yield self.db.setGroupMembers("A", ("B", "C", "D",))
+ yield self.db.setGroupMembers("X", ("B", "C",))
+
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+
+ # Remove and check the result
+ yield self.db.removeGroup("A")
+
+ membersA = yield self.db.getMembers("A")
+ membersX = yield self.db.getMembers("X")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+
+ self.assertEqual(membersA, set())
+ self.assertEqual(membersX, set(("B", "C",)))
+ self.assertEqual(membershipsB, set("X",))
+ self.assertEqual(membershipsC, set("X",))
+ self.assertEqual(membershipsD, set())
+
+ @inlineCallbacks
+ def test_cachingDBRemovePrincipal(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ yield self.db.clean()
+
+ # Do one insert and check the result
+ yield self.db.setGroupMembers("A", ("B", "C", "D",))
+ yield self.db.setGroupMembers("X", ("B", "C",))
+
+ membersA = yield self.db.getMembers("A")
+ membersX = yield self.db.getMembers("X")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+
+ self.assertEqual(membersA, set(("B", "C", "D",)))
+ self.assertEqual(membersX, set(("B", "C",)))
+ self.assertEqual(membershipsB, set(("A", "X",)))
+ self.assertEqual(membershipsC, set(("A", "X",)))
+ self.assertEqual(membershipsD, set(("A",)))
+
+ # Remove and check the result
+ yield self.db.removePrincipal("B")
+
+ membersA = yield self.db.getMembers("A")
+ membersX = yield self.db.getMembers("X")
+ membershipsB = yield self.db.getMemberships("B")
+ membershipsC = yield self.db.getMemberships("C")
+ membershipsD = yield self.db.getMemberships("D")
+
+ self.assertEqual(membersA, set(("C", "D",)))
+ self.assertEqual(membersX, set(("C",)))
+ self.assertEqual(membershipsB, set())
+ self.assertEqual(membershipsC, set(("A", "X",)))
+ self.assertEqual(membershipsD, set(("A",),))
+
+ @inlineCallbacks
+ def test_cachingDBInsertUncached(self):
+
+ for processType in ("Single", "Combined",):
+ config.ProcessType = processType
+
+ # Get the DB
+ yield self.db.clean()
+
+ # Do one insert and check the result for the one we will remove
+ yield self.db.setGroupMembers("AA", ("BB", "CC", "DD",))
+ yield self.db.getMemberships("DD")
+
+ # Change and check the result
+ yield self.db.setGroupMembers("AA", ("BB", "CC", "EE",))
+
+ membersAA = yield self.db.getMembers("AA")
+ membershipsBB = yield self.db.getMemberships("BB")
+ membershipsCC = yield self.db.getMemberships("CC")
+ membershipsDD = yield self.db.getMemberships("DD")
+ membershipsEE = yield self.db.getMemberships("EE")
+
+ self.assertEqual(membersAA, set(("BB", "CC", "EE",)))
+ self.assertEqual(membershipsBB, set(("AA",)))
+ self.assertEqual(membershipsCC, set(("AA",)))
+ self.assertEqual(membershipsDD, set())
+ self.assertEqual(membershipsEE, set(("AA",)))
+
+
+try:
+ import pgdb
+except ImportError:
+ ProxyPrincipalDBPostgreSQL.skip = True
+else:
+ try:
+ db = pgdb.connect(host="localhost", database="proxies")
+ except:
+ ProxyPrincipalDBPostgreSQL.skip = True
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipalmembers.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_proxyprincipalmembers.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -14,39 +14,54 @@
# limitations under the License.
##
-from twisted.internet.defer import DeferredList, inlineCallbacks, succeed
+from twisted.internet.defer import DeferredList, inlineCallbacks, returnValue,\
+ succeed
from twisted.web2.dav import davxml
from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile,\
+ proxiesFile
from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
from twistedcaldav.directory.principal import DirectoryPrincipalResource
+from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
+from twistedcaldav.directory.xmlfile import XMLDirectoryService
import twistedcaldav.test.util
+from twistedcaldav.config import config
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
+import os
-directoryService = XMLDirectoryService(xmlFile)
-
class ProxyPrincipals (twistedcaldav.test.util.TestCase):
"""
Directory service provisioned principals.
"""
+
+ @inlineCallbacks
def setUp(self):
super(ProxyPrincipals, self).setUp()
+ self.directoryService = XMLDirectoryService(xmlFile=xmlFile)
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
+ calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(self.mktemp())
+
# Set up a principals hierarchy for each service we're testing with
self.principalRootResources = {}
- name = directoryService.__class__.__name__
+ name = self.directoryService.__class__.__name__
url = "/" + name + "/"
- provisioningResource = DirectoryPrincipalProvisioningResource(url, directoryService)
+ provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
self.site.resource.putChild(name, provisioningResource)
- self.principalRootResources[directoryService.__class__.__name__] = provisioningResource
+ self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
+ config.DataRoot = self.mktemp()
+ os.mkdir(config.DataRoot)
+ yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
+
def _getPrincipalByShortName(self, type, name):
- provisioningResource = self.principalRootResources[directoryService.__class__.__name__]
+ provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
return provisioningResource.principalForShortName(type, name)
def _groupMembersTest(self, recordType, recordName, subPrincipalName, expectedMembers):
@@ -76,6 +91,43 @@
return d
@inlineCallbacks
+ def _addProxy(self, principal, subPrincipalName, proxyPrincipal):
+
+ if isinstance(principal, tuple):
+ principal = self._getPrincipalByShortName(principal[0], principal[1])
+ principal = principal.getChild(subPrincipalName)
+ members = (yield principal.groupMembers())
+
+ if isinstance(proxyPrincipal, tuple):
+ proxyPrincipal = self._getPrincipalByShortName(proxyPrincipal[0], proxyPrincipal[1])
+ members.add(proxyPrincipal)
+
+ yield principal.setGroupMemberSetPrincipals(members)
+
+ @inlineCallbacks
+ def _removeProxy(self, recordType, recordName, subPrincipalName, proxyRecordType, proxyRecordName):
+
+ principal = self._getPrincipalByShortName(recordType, recordName)
+ principal = principal.getChild(subPrincipalName)
+ members = (yield principal.groupMembers())
+
+ proxyPrincipal = self._getPrincipalByShortName(proxyRecordType, proxyRecordName)
+ for p in members:
+ if p.principalUID() == proxyPrincipal.principalUID():
+ members.remove(p)
+ break
+
+ yield principal.setGroupMemberSetPrincipals(members)
+
+ @inlineCallbacks
+ def _clearProxy(self, principal, subPrincipalName):
+
+ if isinstance(principal, tuple):
+ principal = self._getPrincipalByShortName(principal[0], principal[1])
+ principal = principal.getChild(subPrincipalName)
+ yield principal.setGroupMemberSetPrincipals(set())
+
+ @inlineCallbacks
def _proxyForTest(self, recordType, recordName, expectedProxies, read_write):
principal = self._getPrincipalByShortName(recordType, recordName)
proxies = (yield principal.proxyFor(read_write))
@@ -215,7 +267,7 @@
return succeed(self.members)
- user = self._getPrincipalByShortName(directoryService.recordType_users,
+ user = self._getPrincipalByShortName(self.directoryService.recordType_users,
"cdaboo")
proxyGroup = user.getChild("calendar-proxy-write")
@@ -243,8 +295,9 @@
changedCount = 0
def changed(self):
self.changedCount += 1
+ return succeed(True)
- user = self._getPrincipalByShortName(directoryService.recordType_users, "cdaboo")
+ user = self._getPrincipalByShortName(self.directoryService.recordType_users, "cdaboo")
proxyGroup = user.getChild("calendar-proxy-write")
@@ -253,7 +306,7 @@
oldCacheNotifier = DirectoryPrincipalResource.cacheNotifierFactory
try:
- DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2: notifier)
+ DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2, **kwargs: notifier)
self.assertEquals(notifier.changedCount, 0)
@@ -283,3 +336,187 @@
False
)
+ @inlineCallbacks
+ def test_UserProxy(self):
+
+ for proxyType in ("calendar-proxy-read", "calendar-proxy-write"):
+
+ yield self._addProxy(
+ (DirectoryService.recordType_users, "wsanchez",),
+ proxyType,
+ (DirectoryService.recordType_users, "cdaboo",),
+ )
+
+ yield self._groupMembersTest(
+ DirectoryService.recordType_users, "wsanchez",
+ proxyType,
+ ("Cyrus Daboo",),
+ )
+
+ yield self._addProxy(
+ (DirectoryService.recordType_users, "wsanchez",),
+ proxyType,
+ (DirectoryService.recordType_users, "lecroy",),
+ )
+
+ yield self._groupMembersTest(
+ DirectoryService.recordType_users, "wsanchez",
+ proxyType,
+ ("Cyrus Daboo", "Chris Lecroy",),
+ )
+
+ yield self._removeProxy(
+ DirectoryService.recordType_users, "wsanchez",
+ proxyType,
+ DirectoryService.recordType_users, "cdaboo",
+ )
+
+ yield self._groupMembersTest(
+ DirectoryService.recordType_users, "wsanchez",
+ proxyType,
+ ("Chris Lecroy",),
+ )
+
+ @inlineCallbacks
+ def test_InvalidUserProxy(self):
+
+
+ # Set up the in-memory (non-null) memcacher:
+ config.ProcessType = "Single"
+ calendaruserproxy.ProxyDBService._memcacher._memcacheProtocol = None
+ principal = self._getPrincipalByShortName(
+ DirectoryService.recordType_users, "wsanchez")
+ db = principal._calendar_user_proxy_index()
+
+ # Set the clock to the epoch:
+ theTime = 0
+ db._memcacher.theTime = theTime
+
+
+ for doMembershipFirst in (True, False):
+ for proxyType in ("calendar-proxy-read", "calendar-proxy-write"):
+
+ principal = self._getPrincipalByShortName(DirectoryService.recordType_users, "wsanchez")
+ proxyGroup = principal.getChild(proxyType)
+
+ testPrincipal = self._getPrincipalByShortName(DirectoryService.recordType_users, "cdaboo")
+
+ fakePrincipal = self._getPrincipalByShortName(DirectoryService.recordType_users, "dreid")
+ fakeProxyGroup = fakePrincipal.getChild(proxyType)
+
+ yield self._addProxy(
+ principal,
+ proxyType,
+ testPrincipal,
+ )
+
+ members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+ self.assertEquals(len(members), 1)
+
+ yield self._addProxy(
+ fakePrincipal,
+ proxyType,
+ testPrincipal,
+ )
+ members = yield fakeProxyGroup._index().getMembers(fakeProxyGroup.uid)
+ self.assertEquals(len(members), 1)
+
+ uids = [p.principalUID() for p in (yield testPrincipal.groupMemberships())]
+ self.assertTrue("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1#%s" % (proxyType,) in uids)
+
+ memberships = yield testPrincipal._calendar_user_proxy_index().getMemberships(testPrincipal.principalUID())
+ self.assertEquals(len(memberships), 2)
+
+ yield self._addProxy(
+ principal,
+ proxyType,
+ fakePrincipal,
+ )
+ members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+ self.assertEquals(len(members), 2)
+
+ # Remove the dreid user from the directory service
+ del self.directoryService._accounts()[DirectoryService.recordType_users]["dreid"]
+
+
+ cacheTimeout = config.DirectoryService.params.get("cacheTimeout", 30) * 60 * 2
+
+ @inlineCallbacks
+ def _membershipTest():
+
+ uids = [p.principalUID() for p in (yield testPrincipal.groupMemberships())]
+ self.assertTrue("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1#%s" % (proxyType,) not in uids)
+
+ memberships = yield testPrincipal._calendar_user_proxy_index().getMemberships(testPrincipal.principalUID())
+ self.assertEquals(len(memberships), 1)
+
+ @inlineCallbacks
+ def _membersTest(theTime):
+ yield self._groupMembersTest(
+ DirectoryService.recordType_users, "wsanchez",
+ proxyType,
+ ("Cyrus Daboo",),
+ )
+
+ # Trigger the proxy DB clean up, which won't actually
+ # remove anything because we haven't exceeded the timeout
+ yield proxyGroup.groupMembers()
+
+ # Advance 10 seconds
+ theTime += 10
+ db._memcacher.theTime = theTime
+
+ # When we first examine the members, we have not exceeded
+ # the clean-up timeout, so we'll still have 2:
+ members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+ self.assertEquals(len(members), 2)
+
+ # Restore removed user
+ parser = XMLAccountsParser(self.directoryService.xmlFile)
+ self.directoryService._parsedAccounts = parser.items
+ self.directoryService.recordWithShortName(
+ DirectoryService.recordType_users, "dreid")
+
+ # Trigger the proxy DB clean up, which will actually
+ # remove the deletion timer because the principal has been
+ # restored
+ yield proxyGroup.groupMembers()
+
+ # Verify the deletion timer has been removed
+ result = yield db._memcacher.checkDeletionTimer("5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1")
+ self.assertEquals(result, None)
+
+ # Remove the dreid user from the directory service
+ del self.directoryService._accounts()[DirectoryService.recordType_users]["dreid"]
+
+ # Trigger the proxy DB clean up, which won't actually
+ # remove anything because we haven't exceeded the timeout
+ yield proxyGroup.groupMembers()
+
+ # Advance beyond the timeout
+ theTime += cacheTimeout
+ db._memcacher.theTime = theTime
+
+ # Trigger the proxy DB clean up
+ yield proxyGroup.groupMembers()
+
+ # The missing principal has now been cleaned out of the
+ # proxy DB
+ members = yield proxyGroup._index().getMembers(proxyGroup.uid)
+ self.assertEquals(len(members), 1)
+ returnValue(theTime)
+
+
+ if doMembershipFirst:
+ yield _membershipTest()
+ theTime = yield _membersTest(theTime)
+ else:
+ theTime = yield _membersTest(theTime)
+ yield _membershipTest()
+
+ # Restore removed user
+ parser = XMLAccountsParser(self.directoryService.xmlFile)
+ self.directoryService._parsedAccounts = parser.items
+
+ yield self._clearProxy(principal, proxyType)
+ yield self._clearProxy(fakePrincipal, proxyType)
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_sqldb.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_sqldb.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_sqldb.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,51 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import os
-
-from twisted.python.filepath import FilePath
-
-import twistedcaldav.directory.test.util
-import twistedcaldav.directory.test.test_xmlfile
-from twistedcaldav.directory.sqldb import SQLDirectoryService
-
-xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-class SQLDB (
- twistedcaldav.directory.test.test_xmlfile.XMLFileBase,
- twistedcaldav.directory.test.util.BasicTestCase,
- twistedcaldav.directory.test.util.DigestTestCase
-):
- """
- Test SQL directory implementation.
- """
- def service(self):
- return SQLDirectoryService(os.getcwd(), self.xmlFile())
-
- def test_verifyCredentials_digest(self):
- super(SQLDB, self).test_verifyCredentials_digest()
- test_verifyCredentials_digest.todo = ""
-
- def test_verifyRealmFromDB(self):
- # Make sure the database has been initialized with the XML file
- self.service()
-
- # Then get an instance without using the XML file
- service = SQLDirectoryService(os.getcwd(), None)
-
- self.assertEquals(service.realmName, "Test")
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_util.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_util.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -15,12 +15,11 @@
##
from twistedcaldav.directory.util import uuidFromName
+from twistedcaldav.test.util import TestCase
-import twisted.trial.unittest
-
uuid_namespace_dns = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
-class UUID (twisted.trial.unittest.TestCase):
+class UUID (TestCase):
def test_uuidFromName(self):
self.assertEquals(
uuidFromName(uuid_namespace_dns, "python.org"),
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_xmlfile.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/test_xmlfile.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -18,11 +18,14 @@
from twisted.python.filepath import FilePath
+from twistedcaldav.directory import augment
from twistedcaldav.directory.directory import DirectoryService
import twistedcaldav.directory.test.util
from twistedcaldav.directory.xmlfile import XMLDirectoryService
xmlFile = FilePath(os.path.join(os.path.dirname(__file__), "accounts.xml"))
+augmentsFile = FilePath(os.path.join(os.path.dirname(__file__), "augments.xml"))
+proxiesFile = FilePath(os.path.join(os.path.dirname(__file__), "proxies.xml"))
# FIXME: Add tests for GUID hooey, once we figure out what that means here
@@ -35,13 +38,14 @@
))
users = {
- "admin" : { "password": "nimda", "guid": "D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "addresses": () },
- "wsanchez": { "password": "zehcnasw", "guid": "6423F94A-6B76-4A3A-815B-D52CFD77935D", "addresses": ("mailto:wsanchez at example.com",) },
- "cdaboo" : { "password": "oobadc", "guid": "5A985493-EE2C-4665-94CF-4DFEA3A89500", "addresses": ("mailto:cdaboo at example.com",) },
- "lecroy" : { "password": "yorcel", "guid": "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "addresses": ("mailto:lecroy at example.com",) },
- "dreid" : { "password": "dierd", "guid": "5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "addresses": ("mailto:dreid at example.com",) },
- "user01" : { "password": "01user", "guid": None , "addresses": () },
- "user02" : { "password": "02user", "guid": None , "addresses": () },
+ "admin" : { "password": "nimda", "guid": "D11F03A0-97EA-48AF-9A6C-FAC7F3975766", "addresses": () },
+ "wsanchez" : { "password": "zehcnasw", "guid": "6423F94A-6B76-4A3A-815B-D52CFD77935D", "addresses": ("mailto:wsanchez at example.com",) },
+ "cdaboo" : { "password": "oobadc", "guid": "5A985493-EE2C-4665-94CF-4DFEA3A89500", "addresses": ("mailto:cdaboo at example.com",) },
+ "lecroy" : { "password": "yorcel", "guid": "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0", "addresses": ("mailto:lecroy at example.com",) },
+ "dreid" : { "password": "dierd", "guid": "5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1", "addresses": ("mailto:dreid at example.com",) },
+ "nocalendar" : { "password": "radnelacon", "guid": "543D28BA-F74F-4D5F-9243-B3E3A61171E5", "addresses": () },
+ "user01" : { "password": "01user", "guid": None , "addresses": () },
+ "user02" : { "password": "02user", "guid": None , "addresses": () },
}
groups = {
@@ -83,6 +87,12 @@
xmlFile.copyTo(self._xmlFile)
return self._xmlFile
+ def augmentsFile(self):
+ if not hasattr(self, "_augmentsFile"):
+ self._augmentsFile = FilePath(self.mktemp())
+ augmentsFile.copyTo(self._augmentsFile)
+ return self._augmentsFile
+
class XMLFile (
XMLFileBase,
twistedcaldav.directory.test.util.BasicTestCase,
@@ -92,7 +102,9 @@
Test XML file based directory implementation.
"""
def service(self):
- return XMLDirectoryService(self.xmlFile())
+ directory = XMLDirectoryService(self.xmlFile())
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,))
+ return directory
def test_changedXML(self):
service = self.service()
@@ -103,6 +115,7 @@
<accounts realm="Test Realm">
<user>
<uid>admin</uid>
+ <guid>admin</guid>
<password>nimda</password>
<name>Super User</name>
</user>
@@ -129,13 +142,28 @@
<accounts realm="Test Realm">
<location>
<uid>my office</uid>
+ <guid>myoffice</guid>
<password>nimda</password>
<name>Super User</name>
- <auto-schedule/>
</location>
</accounts>
"""
)
+ self.augmentsFile().open("w").write(
+"""<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE accounts SYSTEM "accounts.dtd">
+<augments>
+ <record>
+ <guid>myoffice</guid>
+ <enable>true</enable>
+ <enable-calendar>true</enable-calendar>
+ <auto-schedule>true</auto-schedule>
+ </record>
+</augments>
+"""
+ )
+ augment.AugmentService.refresh()
+
for recordType, expectedRecords in (
( DirectoryService.recordType_users , () ),
( DirectoryService.recordType_groups , () ),
@@ -148,28 +176,7 @@
)
self.assertTrue(service.recordWithShortName(DirectoryService.recordType_locations, "my office").autoSchedule)
- def test_badAutoSchedule(self):
- service = self.service()
- self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
- <user>
- <uid>my office</uid>
- <password>nimda</password>
- <name>Super User</name>
- <auto-schedule/>
- </user>
-</accounts>
-"""
- )
-
- def _findRecords():
- set(r.shortName for r in service.listRecords(DirectoryService.recordType_users))
-
- self.assertRaises(ValueError, _findRecords)
-
def test_okDisableCalendar(self):
service = self.service()
@@ -186,11 +193,11 @@
<uid>disabled</uid>
<password>disabled</password>
<name>Disabled</name>
- <disable-calendar/>
</group>
</accounts>
"""
)
+
for recordType, expectedRecords in (
( DirectoryService.recordType_users , () ),
( DirectoryService.recordType_groups , ("enabled", "disabled") ),
@@ -201,88 +208,5 @@
set(r.shortName for r in service.listRecords(recordType)),
set(expectedRecords)
)
- self.assertTrue(service.recordWithShortName(DirectoryService.recordType_groups, "enabled").enabledForCalendaring)
+ self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "enabled").enabledForCalendaring)
self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "disabled").enabledForCalendaring)
-
- def test_badDisableCalendar(self):
- service = self.service()
-
- self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
- <location>
- <uid>my office</uid>
- <password>nimda</password>
- <name>Super User</name>
- <disable-calendar/>
- </location>
-</accounts>
-"""
- )
-
- def _findRecords():
- set(r.shortName for r in service.listRecords(DirectoryService.recordType_users))
-
- self.assertRaises(ValueError, _findRecords)
-
- def test_okProxies(self):
- service = self.service()
-
- self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
- <user>
- <uid>test</uid>
- <password>nimda</password>
- <name>Test</name>
- </user>
- <location>
- <uid>my office</uid>
- <password>nimda</password>
- <name>Super User</name>
- <auto-schedule/>
- <proxies>
- <member>test</member>
- </proxies>
- </location>
-</accounts>
-"""
- )
- for recordType, expectedRecords in (
- ( DirectoryService.recordType_users , ("test",) ),
- ( DirectoryService.recordType_groups , () ),
- ( DirectoryService.recordType_locations , ("my office",) ),
- ( DirectoryService.recordType_resources , () ),
- ):
- self.assertEquals(
- set(r.shortName for r in service.listRecords(recordType)),
- set(expectedRecords)
- )
- self.assertEqual(set([("users", "test",)],), service.recordWithShortName(DirectoryService.recordType_locations, "my office")._proxies)
- self.assertEqual(set([("locations", "my office",)],), service.recordWithShortName(DirectoryService.recordType_users, "test")._proxyFor)
-
- def test_badProxies(self):
- service = self.service()
-
- self.xmlFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<accounts realm="Test Realm">
- <user>
- <uid>my office</uid>
- <password>nimda</password>
- <name>Super User</name>
- <proxies>
- <member>12345-GUID-67890</member>
- </proxies>
- </user>
-</accounts>
-"""
- )
-
- def _findRecords():
- set(r.shortName for r in service.listRecords(DirectoryService.recordType_users))
-
- self.assertRaises(ValueError, _findRecords)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/util.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/test/util.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -14,17 +14,17 @@
# limitations under the License.
##
-import twisted.trial.unittest
from twisted.trial.unittest import SkipTest
from twisted.cred.credentials import UsernamePassword
from twisted.web2.auth.digest import DigestedCredentials, calcResponse, calcHA1
from twistedcaldav.directory.directory import DirectoryService
from twistedcaldav.directory.directory import UnknownRecordTypeError
+from twistedcaldav.test.util import TestCase
# FIXME: Add tests for GUID hooey, once we figure out what that means here
-class DirectoryTestCase (twisted.trial.unittest.TestCase):
+class DirectoryTestCase (TestCase):
"""
Tests a directory implementation.
"""
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaccountsparser.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaccountsparser.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -23,7 +23,6 @@
"XMLAccountsParser",
]
-from uuid import UUID
import xml.dom.minidom
from twisted.python.filepath import FilePath
@@ -43,18 +42,17 @@
ELEMENT_GUID = "guid"
ELEMENT_PASSWORD = "password"
ELEMENT_NAME = "name"
+ELEMENT_EMAIL_ADDRESS = "email-address"
ELEMENT_MEMBERS = "members"
ELEMENT_MEMBER = "member"
-ELEMENT_CUADDR = "cuaddr"
-ELEMENT_AUTOSCHEDULE = "auto-schedule"
-ELEMENT_DISABLECALENDAR = "disable-calendar"
-ELEMENT_PROXIES = "proxies"
-ELEMENT_READ_ONLY_PROXIES = "read-only-proxies"
ATTRIBUTE_REALM = "realm"
ATTRIBUTE_REPEAT = "repeat"
ATTRIBUTE_RECORDTYPE = "type"
+VALUE_TRUE = "true"
+VALUE_FALSE = "false"
+
RECORD_TYPES = {
ELEMENT_USER : DirectoryService.recordType_users,
ELEMENT_GROUP : DirectoryService.recordType_groups,
@@ -69,7 +67,8 @@
def __repr__(self):
return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
- def __init__(self, xmlFile):
+ def __init__(self, xmlFile, externalUpdate=True):
+
if type(xmlFile) is str:
xmlFile = FilePath(xmlFile)
@@ -88,7 +87,7 @@
# Verify that top-level element is correct
accounts_node = doc._get_documentElement()
if accounts_node._get_localName() != ELEMENT_ACCOUNTS:
- self.log("Ignoring file %r because it is not a repository builder file" % (self.xmlFile,))
+ log.error("Ignoring file %r because it is not a repository builder file" % (self.xmlFile,))
return
self._parseXML(accounts_node)
@@ -103,23 +102,10 @@
def updateMembership(group):
# Update group membership
for recordType, shortName in group.members:
- item = self.items[recordType].get(shortName, None)
+ item = self.items[recordType].get(shortName)
if item is not None:
item.groups.add(group.shortName)
- def updateProxyFor(proxier):
- # Update proxy membership
- for recordType, shortName in proxier.proxies:
- item = self.items[recordType].get(shortName, None)
- if item is not None:
- item.proxyFor.add((proxier.recordType, proxier.shortName))
-
- # Update read-only proxy membership
- for recordType, shortName in proxier.readOnlyProxies:
- item = self.items[recordType].get(shortName, None)
- if item is not None:
- item.readOnlyProxyFor.add((proxier.recordType, proxier.shortName))
-
for child in node._get_childNodes():
child_name = child._get_localName()
if child_name is None:
@@ -148,7 +134,6 @@
for records in self.items.itervalues():
for principal in records.itervalues():
updateMembership(principal)
- updateProxyFor(principal)
class XMLAccountRecord (object):
"""
@@ -163,15 +148,9 @@
self.guid = None
self.password = None
self.name = None
+ self.emailAddresses = set()
self.members = set()
self.groups = set()
- self.calendarUserAddresses = set()
- self.autoSchedule = False
- self.enabledForCalendaring = True
- self.proxies = set()
- self.proxyFor = set()
- self.readOnlyProxies = set()
- self.readOnlyProxyFor = set()
def repeat(self, ctr):
"""
@@ -195,24 +174,20 @@
name = self.name % ctr
else:
name = self.name
- calendarUserAddresses = set()
- for cuaddr in self.calendarUserAddresses:
- if cuaddr.find("%") != -1:
- calendarUserAddresses.add(cuaddr % ctr)
+ emailAddresses = set()
+ for emailAddr in self.emailAddresses:
+ if emailAddr.find("%") != -1:
+ emailAddresses.add(emailAddr % ctr)
else:
- calendarUserAddresses.add(cuaddr)
+ emailAddresses.add(emailAddr)
result = XMLAccountRecord(self.recordType)
result.shortName = shortName
result.guid = guid
result.password = password
result.name = name
+ result.emailAddresses = emailAddresses
result.members = self.members
- result.calendarUserAddresses = calendarUserAddresses
- result.autoSchedule = self.autoSchedule
- result.enabledForCalendaring = self.enabledForCalendaring
- result.proxies = self.proxies
- result.readOnlyProxies = self.readOnlyProxies
return result
def parseXML(self, node):
@@ -225,43 +200,20 @@
self.shortName = child.firstChild.data.encode("utf-8")
elif child_name == ELEMENT_GUID:
if child.firstChild is not None:
- guid = child.firstChild.data.encode("utf-8")
- try:
- UUID(guid)
- except:
- log.error("Invalid GUID in accounts XML: %r" % (guid,))
- self.guid = guid
+ self.guid = child.firstChild.data.encode("utf-8")
+ if len(self.guid) < 4:
+ self.guid += "?" * (4 - len(self.guid))
elif child_name == ELEMENT_PASSWORD:
if child.firstChild is not None:
self.password = child.firstChild.data.encode("utf-8")
elif child_name == ELEMENT_NAME:
if child.firstChild is not None:
self.name = child.firstChild.data.encode("utf-8")
+ elif child_name == ELEMENT_EMAIL_ADDRESS:
+ if child.firstChild is not None:
+ self.emailAddresses.add(child.firstChild.data.encode("utf-8").lower())
elif child_name == ELEMENT_MEMBERS:
self._parseMembers(child, self.members)
- elif child_name == ELEMENT_CUADDR:
- if child.firstChild is not None:
- self.calendarUserAddresses.add(child.firstChild.data.encode("utf-8").lower())
- elif child_name == ELEMENT_AUTOSCHEDULE:
- # Only Resources & Locations
- if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child_name,))
- self.autoSchedule = True
- elif child_name == ELEMENT_DISABLECALENDAR:
- # Only Users or Groups
- if self.recordType not in (DirectoryService.recordType_users, DirectoryService.recordType_groups):
- raise ValueError("<disable-calendar> element only allowed for Users or Groups: %s" % (child_name,))
- self.enabledForCalendaring = False
- elif child_name == ELEMENT_PROXIES:
- # Only Resources & Locations
- if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child_name,))
- self._parseMembers(child, self.proxies)
- elif child_name == ELEMENT_READ_ONLY_PROXIES:
- # Only Resources & Locations
- if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
- raise ValueError("<auto-schedule> element only allowed for Resources and Locations: %s" % (child_name,))
- self._parseMembers(child, self.readOnlyProxies)
else:
raise RuntimeError("Unknown account attribute: %s" % (child_name,))
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaugmentsparser.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/directory/xmlaugmentsparser.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaugmentsparser.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlaugmentsparser.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,141 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from xml.etree.ElementTree import ElementTree
+from xml.parsers.expat import ExpatError
+import types
+
+"""
+XML based augment configuration file handling.
+"""
+
+__all__ = [
+ "XMLAugmentsParser",
+]
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_AUGMENTS = "augments"
+ELEMENT_RECORD = "record"
+
+ELEMENT_GUID = "guid"
+ELEMENT_ENABLE = "enable"
+ELEMENT_HOSTEDAT = "hosted-at"
+ELEMENT_ENABLECALENDAR = "enable-calendar"
+ELEMENT_AUTOSCHEDULE = "auto-schedule"
+ELEMENT_CUADDR = "cuaddr"
+
+ATTRIBUTE_REPEAT = "repeat"
+
+VALUE_TRUE = "true"
+VALUE_FALSE = "false"
+
+ELEMENT_AUGMENTRECORD_MAP = {
+ ELEMENT_GUID: "guid",
+ ELEMENT_ENABLE: "enabled",
+ ELEMENT_HOSTEDAT: "hostedAt",
+ ELEMENT_ENABLECALENDAR: "enabledForCalendaring",
+ ELEMENT_AUTOSCHEDULE: "autoSchedule",
+ ELEMENT_CUADDR: "calendarUserAddresses",
+}
+
+class XMLAugmentsParser(object):
+ """
+ XML augments configuration file parser.
+ """
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+ def __init__(self, xmlFile, items):
+
+ self.items = items
+ self.xmlFile = xmlFile
+
+ # Read in XML
+ try:
+ tree = ElementTree(file=self.xmlFile)
+ except ExpatError, e:
+ log.error("Unable to parse file '%s' because: %s" % (self.xmlFile, e,), raiseException=RuntimeError)
+
+ # Verify that top-level element is correct
+ augments_node = tree.getroot()
+ if augments_node.tag != ELEMENT_AUGMENTS:
+ log.error("Ignoring file '%s' because it is not a augments file" % (self.xmlFile,), raiseException=RuntimeError)
+
+ self._parseXML(augments_node)
+
+ def _parseXML(self, rootnode):
+ """
+ Parse the XML root node from the augments configuration document.
+ @param rootnode: the L{Element} to parse.
+ """
+ for child in rootnode.getchildren():
+
+ if child.tag != ELEMENT_RECORD:
+ log.error("Unknown augment type: '%s' in augment file: '%s'" % (child.tag, self.xmlFile,), raiseException=RuntimeError)
+
+ repeat = int(child.get(ATTRIBUTE_REPEAT, "1"))
+
+ fields = {}
+ for node in child.getchildren():
+
+ if node.tag in (
+ ELEMENT_GUID,
+ ELEMENT_HOSTEDAT,
+ ):
+ fields[node.tag] = node.text
+ elif node.tag in (
+ ELEMENT_ENABLE,
+ ELEMENT_ENABLECALENDAR,
+ ELEMENT_AUTOSCHEDULE,
+ ):
+ fields[node.tag] = node.text == VALUE_TRUE
+ elif node.tag == ELEMENT_CUADDR:
+ fields.setdefault(node.tag, set()).add(node.text)
+ else:
+ log.error("Invalid element '%s' in augment file: '%s'" % (node.tag, self.xmlFile,), raiseException=RuntimeError)
+
+ # Must have at least a guid
+ if ELEMENT_GUID not in fields:
+ log.error("Invalid record '%s' without a guid in augment file: '%s'" % (child, self.xmlFile,), raiseException=RuntimeError)
+
+ if repeat > 1:
+ for i in xrange(1, repeat+1):
+ self.buildRecord(fields, i)
+ else:
+ self.buildRecord(fields)
+
+ def buildRecord(self, fields, count=None):
+
+ from twistedcaldav.directory.augment import AugmentRecord
+
+ def expandCount(value, count):
+
+ if type(value) in types.StringTypes:
+ return value % (count,) if count and "%" in value else value
+ elif type(value) == set:
+ return set([item % (count,) if count and "%" in item else item for item in value])
+ else:
+ return value
+
+ actualFields = {}
+ for k,v in fields.iteritems():
+ actualFields[ELEMENT_AUGMENTRECORD_MAP[k]] = expandCount(v, count)
+
+ record = AugmentRecord(**actualFields)
+ self.items[record.guid] = record
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlfile.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/directory/xmlfile.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -26,6 +26,7 @@
from twisted.web2.auth.digest import DigestedCredentials
from twisted.python.filepath import FilePath
+from twistedcaldav.directory import augment
from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
@@ -61,23 +62,39 @@
def listRecords(self, recordType):
for entryShortName, xmlPrincipal in self._entriesForRecordType(recordType):
- yield XMLDirectoryRecord(
+ record = XMLDirectoryRecord(
service = self,
recordType = recordType,
shortName = entryShortName,
xmlPrincipal = xmlPrincipal,
)
+ # Look up augment information
+ # TODO: this needs to be deferred but for now we hard code the deferred result because
+ # we know it is completing immediately.
+ d = augment.AugmentService.getAugmentRecord(record.guid)
+ d.addCallback(lambda x:record.addAugmentInformation(x))
+
+ yield record
+
def recordWithShortName(self, recordType, shortName):
for entryShortName, xmlprincipal in self._entriesForRecordType(recordType):
if entryShortName == shortName:
- return XMLDirectoryRecord(
+ record = XMLDirectoryRecord(
service = self,
recordType = recordType,
shortName = entryShortName,
xmlPrincipal = xmlprincipal,
)
+ # Look up augment information
+ # TODO: this needs to be deferred but for now we hard code the deferred result because
+ # we know it is completing immediately.
+ d = augment.AugmentService.getAugmentRecord(record.guid)
+ d.addCallback(lambda x:record.addAugmentInformation(x))
+
+ return record
+
return None
def _entriesForRecordType(self, recordType):
@@ -108,18 +125,12 @@
guid = xmlPrincipal.guid,
shortName = shortName,
fullName = xmlPrincipal.name,
- calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
- autoSchedule = xmlPrincipal.autoSchedule,
- enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
+ emailAddresses = xmlPrincipal.emailAddresses,
)
self.password = xmlPrincipal.password
self._members = xmlPrincipal.members
self._groups = xmlPrincipal.groups
- self._proxies = xmlPrincipal.proxies
- self._proxyFor = xmlPrincipal.proxyFor
- self._readOnlyProxies = xmlPrincipal.readOnlyProxies
- self._readOnlyProxyFor = xmlPrincipal.readOnlyProxyFor
def members(self):
for recordType, shortName in self._members:
@@ -129,26 +140,11 @@
for shortName in self._groups:
yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
- def proxies(self):
- for recordType, shortName in self._proxies:
- yield self.service.recordWithShortName(recordType, shortName)
-
- def proxyFor(self, read_write=True):
- for recordType, shortName in self._proxyFor:
- yield self.service.recordWithShortName(recordType, shortName)
-
- def readOnlyProxies(self):
- for recordType, shortName in self._readOnlyProxies:
- yield self.service.recordWithShortName(recordType, shortName)
-
- def readOnlyProxyFor(self, read_write=True):
- for recordType, shortName in self._readOnlyProxyFor:
- yield self.service.recordWithShortName(recordType, shortName)
-
def verifyCredentials(self, credentials):
- if isinstance(credentials, UsernamePassword):
- return credentials.password == self.password
- if isinstance(credentials, DigestedCredentials):
- return credentials.checkPassword(self.password)
+ if self.enabled:
+ if isinstance(credentials, UsernamePassword):
+ return credentials.password == self.password
+ if isinstance(credentials, DigestedCredentials):
+ return credentials.checkPassword(self.password)
return super(XMLDirectoryRecord, self).verifyCredentials(credentials)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/extensions.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/extensions.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -42,6 +42,10 @@
from twisted.web2.static import MetaDataMixin
from twisted.web2.dav import davxml
from twisted.web2.dav.davxml import dav_namespace
+from twisted.web2.dav.element.base import WebDAVTextElement, WebDAVUnknownElement,\
+ twisted_dav_namespace
+from twisted.web2.dav.davxml import Error
+from twisted.web2.dav.http import ErrorResponse as SuperErrorResponse
from twisted.web2.dav.http import StatusResponse
from twisted.web2.dav.static import DAVFile as SuperDAVFile
from twisted.web2.dav.resource import DAVResource as SuperDAVResource
@@ -855,3 +859,51 @@
for name in self.propertyStore.list()
)
return self._data
+
+class ErrorDescription(WebDAVTextElement):
+ """
+ The human-readable description of a failed precondition
+ """
+ namespace = twisted_dav_namespace
+ name = "error-description"
+ protected = True
+
+
+class ErrorResponse(SuperErrorResponse):
+ """
+ A L{Response} object which contains a status code and a L{davxml.Error}
+ element.
+ Renders itself as a DAV:error XML document.
+ """
+ error = None
+
+ def __init__(self, code, error, description=None):
+ """
+ @param code: a response code.
+ @param error: an L{davxml.WebDAVElement} identifying the error, or a
+ tuple C{(namespace, name)} with which to create an empty element
+ denoting the error. (The latter is useful in the case of
+ preconditions ans postconditions, not all of which have defined
+ XML element classes.)
+ @param description: an optional string that, if present, will get
+ wrapped in a (twisted_dav_namespace, error-description) element.
+ """
+ if type(error) is tuple:
+ xml_namespace, xml_name = error
+ error = WebDAVUnknownElement()
+ error.namespace = xml_namespace
+ error.name = xml_name
+
+ if description:
+ output = Error(error, ErrorDescription(description)).toxml()
+ else:
+ output = Error(error).toxml()
+
+ Response.__init__(self, code=code, stream=output)
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ self.error = error
+
+ def __repr__(self):
+ return "<%s %s %s>" % (self.__class__.__name__, self.code, self.error.sname())
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/ical.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/ical.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -49,6 +49,7 @@
from twistedcaldav.dateops import compareDateTime, normalizeToUTC, timeRangesOverlap
from twistedcaldav.instance import InstanceList
from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
log = Logger()
@@ -1110,6 +1111,43 @@
return None
+ def getAttendeesByInstance(self, makeUnique=False, onlyScheduleAgentServer=False):
+ """
+ Get the attendee values for each instance. Optionally remove duplicates.
+
+ @param makeUnique: if C{True} remove duplicate ATTENDEEs in each component
+ @type makeUnique: C{bool}
+ @param onlyScheduleAgentServer: if C{True} only return ATETNDEEs with SCHEDULE-AGENT=SERVER set
+ @type onlyScheduleAgentServer: C{bool}
+ @return: a list of tuples of (organizer value, recurrence-id)
+ """
+
+ # Extract appropriate sub-component if this is a VCALENDAR
+ if self.name() == "VCALENDAR":
+ result = ()
+ for component in self.subcomponents():
+ if component.name() != "VTIMEZONE":
+ result += component.getAttendeesByInstance(makeUnique, onlyScheduleAgentServer)
+ return result
+ else:
+ result = ()
+ attendees = set()
+ rid = self.getRecurrenceIDUTC()
+ for attendee in tuple(self.properties("ATTENDEE")):
+
+ if onlyScheduleAgentServer:
+ if "SCHEDULE-AGENT" in attendee.params():
+ if attendee.paramValue("SCHEDULE-AGENT") != "SERVER":
+ continue
+
+ cuaddr = attendee.value()
+ if makeUnique and cuaddr in attendees:
+ self.removeProperty(attendee)
+ else:
+ result += ((cuaddr, rid),)
+ attendees.add(cuaddr)
+ return result
+
def getAttendeeProperty(self, match):
"""
Get the attendees matching a value. Works on either a VCALENDAR or on a component.
@@ -1118,18 +1156,10 @@
@return: the string value of the Organizer property, or None
"""
- # FIXME: we should really have a URL class and have it manage comparisons
- # in a sensible fashion.
- def _normalizeCUAddress(addr):
- if addr.startswith("/") or addr.startswith("http:") or addr.startswith("https:"):
- return addr.rstrip("/")
- else:
- return addr
-
# Need to normalize http/https cu addresses
test = set()
for item in match:
- test.add(_normalizeCUAddress(item))
+ test.add(normalizeCUAddr(item))
# Extract appropriate sub-component if this is a VCALENDAR
if self.name() == "VCALENDAR":
@@ -1139,7 +1169,7 @@
else:
# Find the primary subcomponent
for p in self.properties("ATTENDEE"):
- if _normalizeCUAddress(p.value()) in test:
+ if normalizeCUAddr(p.value()) in test:
return p
return None
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/index.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/index.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/index.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -686,7 +686,7 @@
assert resource.isCalendarCollection(), "non-calendar collection resource %s has no index." % (resource,)
super(Index, self).__init__(resource)
- if config.Memcached['ClientEnabled']:
+ if config.Memcached['Pools']['Default']['ClientEnabled']:
self.reserver = MemcachedUIDReserver(self)
else:
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/itip.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/itip.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,953 +0,0 @@
-##
-# Copyright (c) 2006-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-iTIP (RFC2446) processing.
-"""
-
-#
-# This is currently used for handling auto-replies to schedule requests arriving
-# in an inbox. It is called in a delayed fashion via reactor.callLater.
-#
-# We assume that all the components/calendars we deal with have been determined
-# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
-# types, etc.
-#
-# The logic for component matching needs a lot more work as it currently does not
-# know how to deal with overridden instances.
-#
-
-import datetime
-import md5
-import time
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-from twisted.web2.dav import davxml
-from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twisted.web2.dav.util import joinURL
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.resource import AccessDeniedError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
-from twistedcaldav.resource import isCalendarCollectionResource
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
- "handleRequest",
- "canAutoRespond",
-]
-
-class iTipException(Exception):
- pass
-
-def handleRequest(request, principal, inbox, calendar, child):
- """
- Handle an iTIP response automatically.
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- @return: L{Deferred} that is a L{deferredGenerator}
- """
-
- method = calendar.propertyValue("METHOD")
- if method == "REQUEST":
- f = processRequest
- elif method == "ADD":
- f = processAdd
- elif method == "CANCEL":
- f = processCancel
-
- return f(request, principal, inbox, calendar, child)
-
- at inlineCallbacks
-def processRequest(request, principal, inbox, calendar, child):
- """
- Process a METHOD=REQUEST.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 1. If so,
- 1. Remove existing ones in Inbox.
- 2. See if this updates existing ones in free-busy-set calendars.
- 3. Remove existing ones in those calendars.
- 4. See if this fits into a free slot:
- 1. If not, send REPLY with failure status
- 2. If so
- 1. send REPLY with success
- 2. add to f-b-s calendar
- 2. If not,
- 1. remove the one we got - its 'stale'
- 3. Delete the request from the Inbox.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
-
- log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
- processed = "ignored"
-
- # First determine whether this is a full or partial update. A full update is one containing the master
- # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
- # being changed.
-
- new_master = calendar.masterComponent()
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- if new_master:
- # So we have a full update. That means we need to delete any existing events completely and
- # replace with the ones provided so long as the new one is newer.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # See whether the new component is older than any existing ones and throw it away if so
- newinfo = (None,) + getComponentSyncInfo(new_master)
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- if old_master:
- info = getSyncInfo(calmatch, cal)
- else:
- info = None
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- else:
- processed = "older"
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if accepted:
- if calmatch:
- newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
- log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
- else:
- if calmatch:
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
-
- # Send a reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- else:
- # So we have a partial update. That means we have to do partial updates to instances in
- # the existing calendar component.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # Check each component to see whether its new
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- processed = "older"
- new_components = [component for component in calendar.subcomponents()]
- for component in new_components:
- if component.name() == "VTIMEZONE":
- continue
-
- newinfo = (None,) + getComponentSyncInfo(component)
- old_component = findMatchingComponent(component, cal)
- if old_component:
- info = (None,) + getComponentSyncInfo(old_component)
- elif old_master:
- info = (None,) + getComponentSyncInfo(old_master)
- else:
- info = None
-
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- processed = "processed"
- else:
- calendar.removeComponent(component)
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if calmatch:
- # Merge the new instances with the old ones
- mergeComponents(calendar, cal)
- newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
- log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- if accepted:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
-
- # Do reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- newInboxResource(child, newchild)
-
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
-def processAdd(request, principal, inbox, calendar, child):
- """
- Process a METHOD=ADD.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
- log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
- raise NotImplementedError()
-
- at inlineCallbacks
-def processCancel(request, principal, inbox, calendar, child):
- """
- Process a METHOD=CANCEL.
-
- Policy find all components that match UID, SEQ and R-ID and remove them.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 2. Remove existing ones in Inbox.
- 3. See if this updates existing ones in free-busy-set calendars.
- 4. Remove existing ones in those calendars.
- 5. Remove the incoming request.
-
- NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
- instance may be cancelled. What we need to do for this is:
-
- 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
- delete the old one.
- 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
- leave the new and existing ones in the Inbox.
- 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
- remove the overridden component from the f-b-s resource.
- 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
-
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
-
- log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
- processed = "ignored"
-
- # Get all component info for this iTIP message
- newinfo = getSyncInfo(child.fp.basename(), calendar)
- info = getAllInfo(inbox, calendar, child)
-
- # First see if we have a recurrence id which will force extra work
- has_rid = False
- if newinfo[4] is not None:
- has_rid = True
- else:
- for i in info:
- if i[4] is not None:
- has_rid = True
- break
-
- if not has_rid:
- # Compare the new one with each existing one.
- delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
- if delete_child:
- return
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # See whether the current component is older than any existing ones and throw it away if so
- cal = updatecal.iCalendar(calmatch)
- info = getSyncInfo(calmatch, cal)
- if compareSyncInfo(info, newinfo) < 0:
- # Delete existing resource which has been cancelled
- try:
- yield deleteResource(updatecal, calmatch)
- log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
- else:
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
- # So we need to iterate over each iTIP component.
-
- # Get the existing calendar object
- existing_calendar = updatecal.iCalendar(calmatch)
- existing_master = existing_calendar.masterComponent()
- exdates = []
-
- for component in calendar.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- # Find matching component in existing calendar
- old_component = findMatchingComponent(component, existing_calendar)
-
- if old_component:
- # We are cancelling an overridden component, so we need to check the
- # SEQUENCE/DTSAMP with the master.
- if compareComponents(old_component, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # Remove the existing component.
- existing_calendar.removeComponent(old_component)
- elif existing_master:
- # We are trying to CANCEL a non-overridden instance, so we need to
- # check SEQUENCE/DTSTAMP with the master.
- if compareComponents(existing_master, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # If we have any EXDATEs lets add them to the existing calendar object and write
- # it back.
- if exdates:
- if existing_master:
- existing_master.addProperty(Property("EXDATE", exdates))
-
- # See if there are still components in the calendar - we might have deleted the last overridden instance
- # in which case the calendar object is empty (except for VTIMEZONEs).
- if existing_calendar.mainType() is None:
- # Delete the now empty calendar object
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
- else:
- # Update the existing calendar object
- newchild = yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
- log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- at inlineCallbacks
-def checkForReply(request, principal, calendar):
- """
- Check whether a reply to the given iTIP message is needed. We will not process a reply
- if RSVP=FALSE. A reply will either be positive (accepted
- invitation) or negative (denied invitation). In addition we will modify calendar to reflect
- any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
-
- BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
- At the moment we will treat a failure on one instances as a DECLINE of the entire set.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @return: C{True} if a reply is needed, C{False} otherwise.
- """
-
- # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
- accepted = True
-
- # First expand current one to get instances (only go 1 year into the future)
- default_future_expansion_duration = datetime.timedelta(days=356*1)
- expand_max = datetime.date.today() + default_future_expansion_duration
- instances = calendar.expandTimeRanges(expand_max)
-
- # Extract UID from primary component as we want to ignore this one if we match it
- # in any calendars.
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now compare each instance time-range with the index and see if there is an overlap
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- for calURL in fbset:
- testcal = yield request.locateResource(calURL)
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- # Now do search for overlapping time-range
- for instance in instances.instances.itervalues():
- try:
- tr = caldavxml.TimeRange(start="20000101", end="20000101")
- tr.start = instance.start
- tr.end = instance.end
- yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
-
- # If any fbinfo entries exist we have an overlap
- if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
- accepted = False
- break
- except NumberOfMatchesWithinLimits:
- accepted = False
- log.info("Exceeded number of matches whilst trying to find free-time.")
- break
-
- if not accepted:
- break
-
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProps = calendar.getAttendeeProperties(cuas)
- if not attendeeProps:
- returnValue((False, None, accepted))
-
- # Look for specific parameters
- rsvp = True
- for attendeeProp in attendeeProps:
- if "RSVP" in attendeeProp.params():
- if attendeeProp.params()["RSVP"][0] == "FALSE":
- rsvp = False
-
- # Now modify the original component
- del attendeeProp.params()["RSVP"]
-
- if accepted:
- partstat = "ACCEPTED"
- else:
- partstat = "DECLINED"
- for attendeeProp in attendeeProps:
- if "PARTSTAT" in attendeeProp.params():
- attendeeProp.params()["PARTSTAT"][0] = partstat
- else:
- attendeeProp.params()["PARTSTAT"] = [partstat]
-
- # Now create a new calendar object for the reply
-
- # First get useful props from the original
- replycal = calendar.duplicate()
-
- # Change METHOD
- replycal.getProperty("METHOD").setValue("REPLY")
-
- # Change PRODID to this server
- replycal.getProperty("PRODID").setValue(iCalendarProductID)
-
- # Add REQUEST-STATUS
- for component in replycal.subcomponents():
- if accepted:
- component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
- else:
- component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
- # Remove all attendees other than ourselves
- for component in replycal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- attendeeProp = component.getAttendeeProperty(cuas)
- attendees = tuple(component.properties("ATTENDEE"))
- for attendee in attendees:
- if attendeeProp is None or (attendee.value() != attendeeProp.value()):
- component.removeProperty(attendee)
-
- returnValue((rsvp, replycal, accepted))
-
- at inlineCallbacks
-def writeReply(request, principal, replycal, ainbox):
- """
- Write an iTIP message reply into the specified Inbox.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param replycal: the L{Component} for the iTIP message reply.
- @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
- """
-
- # Get the Inbox of the ORGANIZER
- organizer = replycal.getOrganizer()
- assert organizer is not None
- organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
- assert organizerPrincipal is not None
- inboxURL = organizerPrincipal.scheduleInboxURL()
- assert inboxURL
-
- # Determine whether current principal has CALDAV:schedule right on that Inbox
- inbox = yield request.locateResource(inboxURL)
-
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
- except AccessDeniedError:
- log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
- returnValue(None)
-
- # Now deposit the new calendar into the inbox
- result = yield writeResource(request, inboxURL, inbox, None, replycal)
-
- if accountingEnabled("iTIP", organizerPrincipal):
- emitAccounting(
- "iTIP", organizerPrincipal,
- "Originator: %s\nRecipients: %s\n\n%s"
- % (principal.principalURL(), str(organizer), str(replycal))
- )
-
- returnValue(result)
-
- at inlineCallbacks
-def writeResource(request, collURL, collection, name, calendar):
- """
- Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
- resource or by creating a new one.
-
- @param request: the L{IRequest} for the current request.
- @param collURL: the C{str} containing the URL of the calendar collection.
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @param calendar: the L{Component} calendar to write.
- @return: C{tuple} of L{Deferred}, L{CalDAVFile}
- """
-
- # Create a new name if one was not provided
- if name is None:
- name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- newchildURL = joinURL(collURL, name)
- newchild = yield request.locateResource(newchildURL)
-
- # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
- # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
- # NB Only do this when writing to something other than an Inbox or Outbox
- itipper = True
- if collection.isCalendarCollection():
- method = calendar.getProperty("METHOD")
- if method:
- calendar.removeProperty(method)
- itipper = False
-
- # Now write it to the resource
- try:
- yield storeCalendarObjectResource(
- request=request,
- sourcecal = False,
- destination = newchild,
- destination_uri = newchildURL,
- calendardata = str(calendar),
- destinationparent = collection,
- destinationcal = True,
- isiTIP = itipper
- )
- except:
- # FIXME: bare except
- return
-
- returnValue(newchild)
-
-def newInboxResource(child, newchild):
- """
- Copy recipient and organizer properties from one iTIP resource, to another,
- switching them as appropriate for a reply, and also set the state.
-
- @param child: the L{CalDAVFile} for the original iTIP message.
- @param newchild: the L{CalDAVFile} for the iTIP message reply.
- """
- # Make previous Recipient the new Originator
- if child.hasDeadProperty(caldavxml.Recipient):
- recip = child.readDeadProperty(caldavxml.Recipient)
- if recip.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-
- # Make previous Originator the new Recipient
- if child.hasDeadProperty(caldavxml.Originator):
- orig = child.readDeadProperty(caldavxml.Originator)
- if orig.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-
-def deleteResource(collection, name):
- """
- Delete the calendar resource in the specified calendar.
-
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @return: L{Deferred}
- """
-
- delchild = collection.getChild(name)
- index = collection.index()
- index.deleteResource(delchild.fp.basename())
-
- def _deletedResourced(result):
- # Change CTag on the parent calendar collection
- return collection.updateCTag().addCallback(lambda _: result)
-
- d = maybeDeferred(delete, "", delchild.fp, "0")
- d.addCallback(_deletedResourced)
- return d
-
-def canAutoRespond(calendar):
- """
- Check whether the METHOD of this iTIP calendar object is one we can process. Also,
- we will only handle VEVENTs right now.
-
- @param calendar: L{Component} for calendar to examine.
- @return: C{True} if we can auto-respond, C{False} if not.
- """
-
- try:
- method = calendar.propertyValue("METHOD")
- if method not in ("REQUEST", "ADD", "CANCEL"):
- return False
- if calendar.mainType() not in ("VEVENT"):
- return False
- except ValueError:
- return False
-
- return True
-
- at inlineCallbacks
-def processOthersInInbox(info, newinfo, inbox, child):
- # Compare the new one with each existing one.
- delete_child = False
- for i in info:
- # For any that are older, delete them.
- if compareSyncInfo(i, newinfo) < 0:
- try:
- yield deleteResource(inbox, i[0])
- log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- else:
- # For any that are newer or the same, mark the new one to be deleted.
- delete_child = True
-
- # Delete the new one if so marked.
- if delete_child:
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- returnValue(delete_child)
-
- at inlineCallbacks
-def findCalendarMatch(request, principal, calendar):
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
-
- # Find the current recipients calendar-free-busy-set
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- # Find the first calendar in the list with a component matching the one we are processing
- calmatch = None
- updatecal = None
- calURL = None
- for calURL in fbset:
- updatecal = yield request.locateResource(calURL)
- if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
- calmatch = matchComponentInCalendar(updatecal, calendar)
- if calmatch:
- log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
- break
-
- if calmatch is None and len(fbset):
- calURL = fbset[0]
- updatecal = yield request.locateResource(calURL)
-
- returnValue((calmatch, updatecal, calURL))
-
-def matchComponentInCalendar(collection, calendar):
- """
- See if the component in the provided iTIP calendar object matches any in the specified calendar
- collection.
-
- @param collection: L{CalDAVFile} for the calendar collection to examine.
- @param calendar: L{Component} for calendar to examine.
- @return: C{list} of resource names found.
- """
-
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- result = index.resourceNamesForUID(uid)
-
- # There can be only one
- if len(result) > 0:
- return result[0]
- else:
- return None
- except ValueError:
- return None
-
-def findMatchingComponent(component, calendar):
- """
- See if any overridden component in the provided iTIP calendar object matches the specified component.
-
- @param component: the component to try and match.
- @type component: L{Component}
- @param calendar: the calendar to find a match in.
- @type calendar: L{Component}
- @return: L{Component} for matching component,
- or C{None} if not found.
- """
-
- # Extract RECURRENCE-ID value from component
- rid = component.getRecurrenceIDUTC()
-
- # Return the one that matches in the calendar
- return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
- """
- Merge the overridden instance components in newcal into oldcal replacing any
- matching components there.
-
- @param newcal: the new overridden instances to use.
- @type newcal: L{Component}
- @param oldcal: the component to merge into.
- @type oldcal: L{Component}
- """
-
- # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
- # use a TZID that is already specified in the old component set.
-
- # We will update the SEQUENCE on the master to the highest value of the current one on the master
- # or the ones in the components we are changing.
-
- for component in newcal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- rid = component.getRecurrenceIDUTC()
- old_component = oldcal.overriddenComponent(rid)
- if old_component:
- oldcal.removeComponent(old_component)
- oldcal.addComponent(component)
-
-def getAllInfo(collection, calendar, ignore):
- """
- Find each component in the calendar collection that has a matching UID with
- the supplied component, and get useful synchronization details from it, ignoring
- the one with the supplied resource name.
-
- @param collection: the L{CalDAVFile} for the calendar collection.
- @param calendar: the L{Component} for the component being compared with.
- @param ignore: the C{str} containing the name of a resource to ignore,
- or C{None} if none to ignore.
- @return: C{list} of synchronization information for each resource found.
- """
- names = []
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- names = index.resourceNamesForUID(uid)
-
- # Remove the one we want to ignore
- if ignore is not None:
- names = [name for name in names if name != ignore.fp.basename()]
- except ValueError:
- return []
-
- # Now get info for each name
- result = []
- for name in names:
- cal = collection.iCalendar(name)
- result.append(getSyncInfo(name, cal))
-
- return result
-
-def getSyncInfo(name, calendar):
- """
- Get property value details needed to synchronize iTIP components.
-
- @param calendar: L{Component} for calendar to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract components from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
-
- except ValueError:
- return (name, None, None, None, None)
-
- return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
- """
- Get property value details needed to synchronize iTIP components.
-
- @param component: L{Component} to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract items from component
- uid = component.propertyValue("UID")
- seq = component.propertyValue("SEQUENCE")
- if seq:
- seq = int(seq)
- dtstamp = component.propertyValue("DTSTAMP")
- rid = component.propertyValue("RECURRENCE-ID")
-
- except ValueError:
- return (None, None, None, None)
-
- return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
- """
- Compare synchronization information for two components to see if they match according to iTIP.
-
- @param component1: first component to check.
- @type component1: L{Component}
- @param component2: second component to check.
- @type component2: L{Component}
-
- @return: 0, 1, -1 as per compareSyncInfo.
- """
- info1 = (None,) + getComponentSyncInfo(component1)
- info2 = (None,) + getComponentSyncInfo(component2)
- return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
- """
- Compare two synchronization information records.
-
- @param info1: a C{tuple} as returned by L{getSyncInfo}.
- @param info2: a C{tuple} as returned by L{getSyncInfo}.
- @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
- """
- # UIDs MUST match
- assert info1[1] == info2[1]
-
- # Look for sequence
- if (info1[2] is not None) and (info2[2] is not None):
- if info1[2] > info2[2]:
- return 1
- if info1[2] < info2[2]:
- return -1
- elif (info1[2] is not None) and (info2[2] is None):
- return 1
- elif (info1[2] is None) and (info2[2] is not None):
- return -1
-
- # Look for DTSTAMP
- if (info1[3] is not None) and (info2[3] is not None):
- if info1[3] > info2[3]:
- return 1
- if info1[3] < info2[3]:
- return -1
- elif (info1[3] is not None) and (info2[3] is None):
- return 1
- elif (info1[3] is None) and (info2[3] is not None):
- return -1
-
- return 0
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/log.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/log.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -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.
@@ -55,10 +55,14 @@
"LoggingMixIn",
]
+from StringIO import StringIO
+from twisted.internet.defer import succeed
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
import inspect
-from twisted.python import log
-
logLevels = (
"debug",
"info",
@@ -193,6 +197,86 @@
"""
return cmpLogLevels(self.level(), level) <= 0
+ # FIXME: This doesn't belong here
+ def logRequest(self, level, message, request, **kwargs):
+ """
+ Log an HTTP request.
+ """
+
+ assert level in logLevels
+
+ if self.willLogAtLevel(level):
+ iostr = StringIO()
+ iostr.write("%s\n" % (message,))
+ if hasattr(request, "clientproto"):
+ protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+ else:
+ protocol = "HTTP/1.1"
+ iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+ for name, valuelist in request.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("Authorization",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the request stream as we can only read it once. So we
+ # read it, store the value in a MemoryStream, and replace the request's stream with that,
+ # so the data can be read again.
+ def _gotData(data):
+ iostr.write(data)
+
+ request.stream = MemoryStream(data if data is not None else "")
+ request.stream.doStartReading = None
+
+ self.emit(level, iostr.getvalue(), **kwargs)
+
+ d = allDataFromStream(request.stream)
+ d.addCallback(_gotData)
+ return d
+
+ else:
+ return succeed(None)
+
+ # FIXME: This doesn't belong here
+ def logResponse(self, level, message, response, **kwargs):
+ """
+ Log an HTTP request.
+ """
+
+ assert level in logLevels
+
+ if self.willLogAtLevel(level):
+ iostr = StringIO()
+ iostr.write("%s\n" % (message,))
+ code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+ iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+ for name, valuelist in response.headers.getAllRawHeaders():
+ for value in valuelist:
+ # Do not log authorization details
+ if name not in ("WWW-Authenticate",):
+ iostr.write("%s: %s\n" % (name, value))
+ else:
+ iostr.write("%s: xxxxxxxxx\n" % (name,))
+ iostr.write("\n")
+
+ # We need to play a trick with the response stream to ensure we don't mess it up. So we
+ # read it, store the value in a MemoryStream, and replace the response's stream with that,
+ # so the data can be read again.
+ def _gotData(data):
+ iostr.write(data)
+
+ response.stream = MemoryStream(data if data is not None else "")
+ response.stream.doStartReading = None
+
+ self.emit(level, iostr.getvalue(), **kwargs)
+
+ d = allDataFromStream(response.stream)
+ d.addCallback(_gotData)
+ return d
+
class LoggingMixIn (object):
"""
Mix-in class for logging methods.
@@ -224,8 +308,10 @@
#
# Attach methods to Logger
#
- def log_emit(self, message, level=level, **kwargs):
+ def log_emit(self, message, level=level, raiseException=None, **kwargs):
self.emit(level, message, **kwargs)
+ if raiseException:
+ raise raiseException(message)
log_emit.__doc__ = doc
@@ -234,8 +320,10 @@
#
# Attach methods to LoggingMixIn
#
- def log_emit(self, message, level=level, **kwargs):
+ def log_emit(self, message, level=level, raiseException=None, **kwargs):
self.logger.emit(level, message, **kwargs)
+ if raiseException:
+ raise raiseException(message)
log_emit.__doc__ = doc
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcachepool.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcachepool.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcachepool.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -15,6 +15,7 @@
##
from twisted.python.failure import Failure
+from twisted.internet.address import IPv4Address
from twisted.internet.defer import Deferred, fail
from twisted.internet.protocol import ReconnectingClientFactory
@@ -343,22 +344,46 @@
@ivar _cachePool: A saved cachePool.
"""
_cachePool = None
+ _cachePoolHandle = "Default"
def getCachePool(self):
if self._cachePool is None:
- return defaultCachePool()
+ return defaultCachePool(self._cachePoolHandle)
return self._cachePool
-_memCachePool = None
+_memCachePools = {} # Maps a name to a pool object
+_memCachePoolHandler = {} # Maps a handler id to a named pool
-def installPool(serverAddress, maxClients=5, reactor=None):
- global _memCachePool
- _memCachePool = MemCachePool(serverAddress,
+def installPools(pools, maxClients=5, reactor=None):
+
+ for name, pool in pools.items():
+ if pool["ClientEnabled"]:
+ _installPool(
+ name,
+ pool["HandleCacheTypes"],
+ IPv4Address(
+ "TCP",
+ pool["BindAddress"],
+ pool["Port"],
+ ),
+ maxClients,
+ reactor,
+ )
+
+def _installPool(name, handleTypes, serverAddress, maxClients=5, reactor=None):
+
+ pool = MemCachePool(serverAddress,
maxClients=maxClients,
reactor=None)
+ _memCachePools[name] = pool
-def defaultCachePool():
- return _memCachePool
+ for handle in handleTypes:
+ _memCachePoolHandler[handle] = pool
+
+def defaultCachePool(name):
+ if name not in _memCachePoolHandler:
+ name = "Default"
+ return _memCachePoolHandler[name]
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacheprops.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacheprops.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacheprops.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -60,7 +60,9 @@
log.info("Instantiating memcache connection for MemcachePropertyCollection")
- MemcachePropertyCollection._memcacheClient = MemcacheClientFactory.getClient(["%s:%s" % (config.Memcached.BindAddress, config.Memcached.Port)],
+ MemcachePropertyCollection._memcacheClient = MemcacheClientFactory.getClient([
+ "%s:%s" % (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)
+ ],
debug=0,
pickleProtocol=2,
)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacher.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacher.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/memcacher.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -82,6 +82,7 @@
@type no_invalidation: C{bool}
"""
self._memcacheProtocol = None
+ self._cachePoolHandle = namespace
self._namespace = namespace
self._pickle = pickle
self._noInvalidation = no_invalidation
@@ -91,7 +92,7 @@
if self._memcacheProtocol is not None:
return self._memcacheProtocol
- if config.Memcached['ClientEnabled']:
+ if config.Memcached['Pools']['Default']['ClientEnabled']:
return self.getCachePool()
elif config.ProcessType == "Single" or self._noInvalidation:
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/method/report_common.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/method/report_common.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -287,7 +287,7 @@
fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
- excludeuid=None, organizer=None, same_calendar_user=False):
+ excludeuid=None, organizer=None, organizerPrincipal=None, same_calendar_user=False):
"""
Run a free busy report on the specified calendar collection
accumulating the free busy info for later processing.
@@ -306,7 +306,7 @@
# First check the privilege on this collection
try:
- d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
+ d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), principal=organizerPrincipal))
yield d
d.getResult()
except AccessDeniedError:
@@ -370,7 +370,7 @@
child = child.getResult()
try:
- d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
+ d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces, principal=organizerPrincipal))
yield d
d.getResult()
except AccessDeniedError:
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/notify.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/notify.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/notify.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1295,13 +1295,10 @@
#
# Configure Memcached Client Pool
#
- if config.Memcached.ClientEnabled:
- memcachepool.installPool(
- IPv4Address(
- 'TCP',
- config.Memcached.BindAddress,
- config.Memcached.Port),
- config.Memcached.MaxClients)
+ memcachepool.installPools(
+ config.Memcached.Pools,
+ config.Memcached.MaxClients,
+ )
multiService = service.MultiService()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/partitions.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/partitions.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/partitions.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/partitions.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,71 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.client.pool import installPool
+from twistedcaldav.log import Logger
+from twistedcaldav.py.plistlib import readPlist
+
+"""
+Collection of classes for managing partition information for a group of servers.
+"""
+
+log = Logger()
+
+class Partitions(object):
+
+ def __init__(self):
+
+ self.clear()
+
+ def clear(self):
+ self.partitions = {}
+ self.ownUID = ""
+ self.maxClients = 5
+
+ def readConfig(self, plistpath):
+ try:
+ dataDict = readPlist(plistpath)
+ except (IOError, OSError):
+ log.error("Configuration file does not exist or is inaccessible: %s" % (self._configFileName,))
+ return
+
+ for partition in dataDict.get("partitions", ()):
+ uid = partition.get("uid", None)
+ url = partition.get("url", None)
+ if uid and url:
+ self.partitions[uid] = url
+
+ def setSelfPartition(self, uid):
+ self.ownUID = uid
+
+ def setMaxClients(self, maxClients):
+ self.maxClients = maxClients
+
+ def getPartitionURL(self, uid):
+ # When the UID matches this server return an empty string
+ return self.partitions.get(uid, None) if uid != self.ownUID else ""
+
+ def installReverseProxies(self):
+
+ for partition, url in self.partitions.iteritems():
+ if partition != self.ownUID:
+ installPool(
+ partition,
+ url,
+ self.maxClients,
+ )
+
+partitions = Partitions()
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/root.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/root.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/root.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -71,7 +71,7 @@
self.contentFilters = []
- if config.Memcached['ClientEnabled']:
+ if config.Memcached['Pools']['Default']['ClientEnabled']:
self.responseCache = MemcacheResponseCache(self.fp)
CalendarHomeFile.cacheNotifierFactory = MemcacheChangeNotifier
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/schedule.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/schedule.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,5 +1,5 @@
##
-# Copyright (c) 2005-2008 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.
@@ -21,36 +21,31 @@
__all__ = [
"ScheduleInboxResource",
"ScheduleOutboxResource",
+ "IScheduleInboxResource",
]
-import md5
-import time
-
from twisted.internet import reactor
-from twisted.internet.defer import maybeDeferred, succeed, inlineCallbacks, returnValue,\
- Deferred
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
from twisted.python.failure import Failure
from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
from twisted.web2.dav import davxml
from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
from twisted.web2.dav.idav import IDAVResource
from twisted.web2.dav.resource import AccessDeniedError
from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
from twistedcaldav import caldavxml
-from twistedcaldav import itip
-from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.caldavxml import caldav_namespace
from twistedcaldav.config import config
from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import Component
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.log import LoggingMixIn
+from twistedcaldav.resource import CalDAVResource
from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.itip import handleRequest
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
+
class CalendarSchedulingCollectionResource (CalDAVResource):
"""
@@ -205,7 +200,7 @@
# Now process each one in order
for child, calendar in children:
- reactor.callLater(0.0, itip.handleRequest, *(request, principal, self, calendar, child)) #@UndefinedVariable
+ reactor.callLater(0.0, handleRequest, *(request, principal, self, calendar, child)) #@UndefinedVariable
if (len(children)):
response_text = "Started auto-processing of %d iTIP messages." % (len(children,))
@@ -270,345 +265,14 @@
# Check authentication and access controls
yield self.authorize(request, (caldavxml.Schedule(),))
- # Must be content-type text/calendar
- contentType = request.headers.getHeader("content-type")
- if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
- self.log_error("MIME type %s not allowed in calendar collection" % (contentType,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-
- # Must have Originator header
- originator = request.headers.getRawHeaders("originator")
- if originator is None or (len(originator) != 1):
- self.log_error("POST request must have Originator header")
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
- else:
- originator = originator[0]
-
- # Verify that Originator is a valid calendar user (has an INBOX)
- originatorPrincipal = self.principalForCalendarUserAddress(originator)
- if originatorPrincipal is None:
- self.log_error("Could not find principal for originator: %s" % (originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ # This is a local CALDAV scheduling operation.
+ scheduler = CalDAVScheduler(request, self)
- inboxURL = originatorPrincipal.scheduleInboxURL()
- if inboxURL is None:
- self.log_error("Could not find inbox for originator: %s" % (originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- # Verify that Originator matches the authenticated user
- if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != self.currentPrincipal(request):
- self.log_error("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ # Do the POST processing treating
+ result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
+ returnValue(result.response())
- # Get list of Recipient headers
- rawRecipients = request.headers.getRawHeaders("recipient")
- if rawRecipients is None or (len(rawRecipients) == 0):
- self.log_error("POST request must have at least one Recipient header")
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
- # Recipient header may be comma separated list
- recipients = []
- for rawRecipient in rawRecipients:
- for r in rawRecipient.split(","):
- r = r.strip()
- if len(r):
- recipients.append(r)
-
- timeRange = TimeRange(start="20000101", end="20000102")
- recipientsState = {"OK":0, "BAD":0}
-
- # Parse the calendar object from the HTTP request stream
- try:
- calendar = yield Component.fromIStream(request.stream)
- calendardata = str(calendar)
- except:
- self.log_error("Error while handling POST: %s" % (Failure(),))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Must be a valid calendar
- try:
- calendar.validCalendarForCalDAV()
- except ValueError:
- self.log_error("POST request calendar component is not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Must have a METHOD
- if not calendar.isValidMethod():
- self.log_error("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # Verify iTIP behaviour
- if not calendar.isValidITIP():
- self.log_error("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-
- # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
- if calendar.hasProperty(Component.ACCESS_PROPERTY):
- self.log_error("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-
- # Verify that the ORGANIZER's cu address maps to the request.uri
- organizer = calendar.getOrganizer()
- if organizer is None:
- organizerPrincipal = None
- else:
- organizerPrincipal = self.principalForCalendarUserAddress(organizer)
-
- if organizerPrincipal is None:
- self.log_error("ORGANIZER in calendar data is not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
- # Prevent spoofing of ORGANIZER with specific METHODs
- if (
- calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER") and
- organizerPrincipal.record != self.parent.record
- ):
- self.log_error("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
- # Prevent spoofing when doing reply-like METHODs
- if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
- # Verify that there is a single ATTENDEE property and that the Originator has permission
- # to send on behalf of that ATTENDEE
- attendees = calendar.getAttendees()
-
- # Must have only one
- if len(attendees) != 1:
- self.log_error("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
- # Attendee's Outbox MUST be the request URI
- attendeePrincipal = self.principalForCalendarUserAddress(attendees[0])
- if attendeePrincipal is None or attendeePrincipal.record != self.parent.record:
- self.log_error("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
- # For free-busy do immediate determination of iTIP result rather than fan-out
- self.log_debug("METHOD: %s, Component: %s" % (calendar.propertyValue("METHOD"), calendar.mainType(),))
- if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
- # Extract time range from VFREEBUSY object
- vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
- if len(vfreebusies) != 1:
- self.log_error("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- dtstart = vfreebusies[0].getStartDateUTC()
- dtend = vfreebusies[0].getEndDateUTC()
- if dtstart is None or dtend is None:
- self.log_error("VFREEBUSY start/end not valid: %s" % (calendar,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- timeRange.start = dtstart
- timeRange.end = dtend
-
- # Look for masked UID
- excludeUID = calendar.getMaskUID()
-
- # Do free busy operation
- freebusy = True
- else:
- # Do regular invite (fan-out)
- freebusy = False
-
- #
- # Accounting
- #
- # Note that we associate logging with the organizer, not the
- # originator, which is good for looking for why something
- # shows up in a given principal's calendars, rather than
- # tracking the activities of a specific user.
- #
- if not hasattr(request, "extendedLogItems"):
- request.extendedLogItems = {}
- if freebusy:
- if accountingEnabled("iTIP-VFREEBUSY", organizerPrincipal):
- emitAccounting(
- "iTIP-VFREEBUSY", organizerPrincipal,
- "Originator: %s\nRecipients: %s\n\n%s"
- % (originator, ", ".join(recipients), calendardata)
- )
- request.extendedLogItems["freebusy"] = len(recipients)
-
- else:
- if accountingEnabled("iTIP", organizerPrincipal):
- emitAccounting(
- "iTIP", organizerPrincipal,
- "Originator: %s\nRecipients: %s\n\n%s"
- % (originator, ", ".join(recipients), calendardata)
- )
- request.extendedLogItems["itip.%s" % (calendar.propertyValue("METHOD").lower(),)] = len(recipients)
- request.extendedLogItems["cl"] = str(len(calendardata))
-
- # Prepare for multiple responses
- responses = ScheduleResponseQueue("POST", responsecode.OK)
-
- # Loop over each recipient and do appropriate action.
- autoresponses = []
- for recipient in recipients:
-
- # Yield to the reactor once through each loop if more than one attendee
- if len(recipients) > 1:
- d = Deferred()
- def _timedDeferred():
- d.callback(True)
- reactor.callLater(0.0, _timedDeferred)
- yield d
-
- # Get the principal resource for this recipient
- principal = self.principalForCalendarUserAddress(recipient)
-
- # Map recipient to their inbox
- inbox = None
- if principal is None:
- self.log_error("No schedulable principal for calendar user address: %r" % (recipient,))
- else:
- inboxURL = principal.scheduleInboxURL()
- if inboxURL:
- inbox = yield request.locateResource(inboxURL)
- else:
- self.log_error("No schedule inbox for principal: %s" % (principal,))
-
- if inbox is None:
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
- recipientsState["BAD"] += 1
-
- # Process next recipient
- continue
- else:
- #
- # Check access controls
- #
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(organizerPrincipal.principalURL())))
- except AccessDeniedError:
- self.log_error("Could not access Inbox for recipient: %s" % (recipient,))
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipientsState["BAD"] += 1
-
- # Process next recipient
- continue
-
- # Different behaviour for free-busy vs regular invite
- if freebusy:
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProp = calendar.getAttendeeProperty(cuas)
-
- # Find the current recipients calendar-free-busy-set
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- try:
- # Process the availability property from the Inbox.
- has_prop = yield inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request)
- if has_prop:
- availability = yield inbox.readProperty((calendarserver_namespace, "calendar-availability"), request)
- availability = availability.calendar()
- report_common.processAvailabilityFreeBusy(availability, fbinfo, timeRange)
-
- # Check to see if the recipient is the same calendar user as the organizer.
- # Needed for masked UID stuff.
- same_calendar_user = organizerPrincipal.principalURL() == principal.principalURL()
-
- # Now process free-busy set calendars
- matchtotal = 0
- for calendarResourceURL in fbset:
-
- calendarResource = yield request.locateResource(calendarResourceURL)
- if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
-
- matchtotal = yield report_common.generateFreeBusyInfo(
- request,
- calendarResource,
- fbinfo,
- timeRange,
- matchtotal,
- excludeuid = excludeUID,
- organizer = organizer,
- same_calendar_user = same_calendar_user
- )
-
- # Build VFREEBUSY iTIP reply for this recipient
- fbresult = report_common.buildFreeBusyResult(
- fbinfo,
- timeRange,
- organizer = calendar.getOrganizerProperty(),
- attendee = attendeeProp,
- uid = calendar.resourceUID(),
- method="REPLY"
- )
-
- responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
- recipientsState["OK"] += 1
-
- except:
- self.log_error("Could not determine free busy information: %s" % (recipient,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipientsState["BAD"] += 1
-
- else:
- # Hash the iCalendar data for use as the last path element of the URI path
- name = md5.new(calendardata + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- childURL = joinURL(inboxURL, name)
- child = yield request.locateResource(childURL)
-
- try:
- # Copy calendar to inbox (doing fan-out)
- yield maybeDeferred(
- storeCalendarObjectResource,
- request=request,
- sourcecal = False,
- destination = child,
- destination_uri = childURL,
- calendardata = calendardata,
- destinationparent = inbox,
- destinationcal = True,
- isiTIP = True
- )
- except: # FIXME: bare except
- self.log_error("Could not store data in Inbox : %s" % (inbox,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
- recipientsState["BAD"] += 1
- else:
- responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
- recipientsState["OK"] += 1
-
- # Store CALDAV:originator property
- child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
-
- # Store CALDAV:recipient property
- child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
-
- # Look for auto-schedule option
- if principal.autoSchedule():
- autoresponses.append((principal, inbox, child))
-
- # Now we have to do auto-respond
- if len(autoresponses) != 0:
- # First check that we have a method that we can auto-respond to
- if not itip.canAutoRespond(calendar):
- autoresponses = []
- else:
- request.extendedLogItems["itip.auto"] = len(autoresponses)
-
- # Now do the actual auto response
- for principal, inbox, child in autoresponses:
- # Add delayed reactor task to handle iTIP responses
- reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
- #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-
- # Return with final response if we are done
- returnValue(responses.response())
-
class ScheduleResponseResponse (Response):
"""
ScheduleResponse L{Response} object.
@@ -696,3 +360,79 @@
return ScheduleResponseResponse(self.responses, self.location)
else:
return self.success_response
+
+class IScheduleInboxResource (CalDAVResource):
+ """
+ iSchedule Inbox resource.
+
+ Extends L{DAVResource} to provide iSchedule inbox functionality.
+ """
+
+ def __init__(self, parent):
+ """
+ @param parent: the parent resource of this one.
+ """
+ assert parent is not None
+
+ CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+ self.parent = parent
+
+ def defaultAccessControlList(self):
+ privs = (
+ davxml.Privilege(davxml.Read()),
+ davxml.Privilege(caldavxml.Schedule()),
+ )
+ if config.Scheduling.CalDAV.OldDraftCompatibility:
+ privs += (davxml.Privilege(caldavxml.Schedule()),)
+
+ return davxml.ACL(
+ # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+ davxml.ACE(
+ davxml.Principal(davxml.All()),
+ davxml.Grant(*privs),
+ davxml.Protected(),
+ ),
+ )
+
+ def resourceType(self):
+ return davxml.ResourceType.ischeduleinbox
+
+ def isCollection(self):
+ return False
+
+ def isCalendarCollection(self):
+ return False
+
+ def isPseudoCalendarCollection(self):
+ return False
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
+
+ @inlineCallbacks
+ def http_POST(self, request):
+ """
+ The server-to-server POST method.
+ """
+
+ # Check authentication and access controls
+ yield self.authorize(request, (caldavxml.Schedule(),))
+
+ # This is a server-to-server scheduling operation.
+ scheduler = IScheduleScheduler(request, self)
+
+ # Do the POST processing treating this as a non-local schedule
+ result = (yield scheduler.doSchedulingViaPOST(use_request_headers=True))
+ returnValue(result.response())
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-CalDAV scheduling.
-"""
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+CalDAV scheduling.
+"""
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/addressmapping.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,91 +0,0 @@
-##
-# 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twistedcaldav.config import config
-from twistedcaldav.log import Logger
-from twistedcaldav.memcacher import Memcacher
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
- RemoteCalendarUser, InvalidCalendarUser, PartitionedCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-
-__all__ = [
- "ScheduleAddressMapper",
- "mapper",
-]
-
-log = Logger()
-
-"""
-Handle mapping a calendar user address to a schedule delivery type.
-"""
-
-class ScheduleAddressMapper(object):
- """
- Class that maps a calendar user address into a delivery service type.
- """
-
- def __init__(self):
-
- # We are going to cache mappings whilst running
- self.cache = Memcacher("ScheduleAddressMapper", no_invalidation=True)
-
- @inlineCallbacks
- def getCalendarUser(self, cuaddr, principal):
-
- # If we have a principal always treat the user as local or partitioned
- if principal:
- returnValue(LocalCalendarUser(cuaddr, principal) if principal.locallyHosted() else PartitionedCalendarUser(cuaddr, principal))
-
- # Get the type
- cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
- if cuaddr_type == DeliveryService.serviceType_caldav:
- returnValue(InvalidCalendarUser(cuaddr))
- elif cuaddr_type == DeliveryService.serviceType_ischedule:
- returnValue(RemoteCalendarUser(cuaddr))
- else:
- returnValue(InvalidCalendarUser(cuaddr))
-
- @inlineCallbacks
- def getCalendarUserServiceType(self, cuaddr):
-
- # Try cache first
- cuaddr_type = (yield self.cache.get(str(cuaddr)))
- if cuaddr_type is None:
-
- serviceTypes = (ScheduleViaCalDAV,)
- if config.Scheduling[DeliveryService.serviceType_ischedule]["Enabled"]:
- serviceTypes += (ScheduleViaISchedule,)
- for service in serviceTypes:
- if service.matchCalendarUserAddress(cuaddr):
- yield self.cache.set(str(cuaddr), service.serviceType())
- returnValue(service.serviceType())
-
- returnValue(cuaddr_type)
-
- def isCalendarUserInMyDomain(self, cuaddr):
-
- # Check whether it is a possible local address
- def _gotResult(serviceType):
- return serviceType == DeliveryService.serviceType_caldav
-
- d = self.getCalendarUserServiceType(cuaddr)
- d.addCallback(_gotResult)
- return d
-
-mapper = ScheduleAddressMapper()
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/addressmapping.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/addressmapping.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,91 @@
+##
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
+ RemoteCalendarUser, InvalidCalendarUser, PartitionedCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+
+__all__ = [
+ "ScheduleAddressMapper",
+ "mapper",
+]
+
+log = Logger()
+
+"""
+Handle mapping a calendar user address to a schedule delivery type.
+"""
+
+class ScheduleAddressMapper(object):
+ """
+ Class that maps a calendar user address into a delivery service type.
+ """
+
+ def __init__(self):
+
+ # We are going to cache mappings whilst running
+ self.cache = Memcacher("ScheduleAddressMapper", no_invalidation=True)
+
+ @inlineCallbacks
+ def getCalendarUser(self, cuaddr, principal):
+
+ # If we have a principal always treat the user as local or partitioned
+ if principal:
+ returnValue(LocalCalendarUser(cuaddr, principal) if principal.locallyHosted() else PartitionedCalendarUser(cuaddr, principal))
+
+ # Get the type
+ cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
+ if cuaddr_type == DeliveryService.serviceType_caldav:
+ returnValue(InvalidCalendarUser(cuaddr))
+ elif cuaddr_type == DeliveryService.serviceType_ischedule:
+ returnValue(RemoteCalendarUser(cuaddr))
+ else:
+ returnValue(InvalidCalendarUser(cuaddr))
+
+ @inlineCallbacks
+ def getCalendarUserServiceType(self, cuaddr):
+
+ # Try cache first
+ cuaddr_type = (yield self.cache.get(str(cuaddr)))
+ if cuaddr_type is None:
+
+ serviceTypes = (ScheduleViaCalDAV,)
+ if config.Scheduling[DeliveryService.serviceType_ischedule]["Enabled"]:
+ serviceTypes += (ScheduleViaISchedule,)
+ for service in serviceTypes:
+ if service.matchCalendarUserAddress(cuaddr):
+ yield self.cache.set(str(cuaddr), service.serviceType())
+ returnValue(service.serviceType())
+
+ returnValue(cuaddr_type)
+
+ def isCalendarUserInMyDomain(self, cuaddr):
+
+ # Check whether it is a possible local address
+ def _gotResult(serviceType):
+ return serviceType == DeliveryService.serviceType_caldav
+
+ d = self.getCalendarUserServiceType(cuaddr)
+ d.addCallback(_gotResult)
+ return d
+
+mapper = ScheduleAddressMapper()
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/caldav.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,267 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-from twistedcaldav.extensions import ErrorResponse
-
-import time
-
-try:
- from hashlib import md5
-except ImportError:
- from md5 import new as md5
-
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
-from twisted.python.failure import Failure
-from twisted.web2 import responsecode
-from twisted.web2.dav import davxml
-from twisted.web2.dav.resource import AccessDeniedError
-from twisted.web2.dav.util import joinURL
-from twisted.web2.http import HTTPError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.log import Logger
-from twistedcaldav.method import report_common
-from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser,\
- PartitionedCalendarUser
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.scheduling.itip import handleRequest
-
-"""
-Class that handles delivery of scheduling messages via CalDAV.
-"""
-
-__all__ = [
- "ScheduleViaCalDAV",
-]
-
-log = Logger()
-
-class ScheduleViaCalDAV(DeliveryService):
-
- def __init__(self, scheduler, recipients, responses, freebusy):
-
- self.scheduler = scheduler
- self.recipients = recipients
- self.responses = responses
- self.freebusy = freebusy
-
- @classmethod
- def serviceType(cls):
- return DeliveryService.serviceType_caldav
-
- @classmethod
- def matchCalendarUserAddress(cls, cuaddr):
-
- # Check for local address matches first
- if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
- splits = cuaddr[7:].split("?")
- domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
- if splits[0].endswith(domain):
- return True
-
- elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
- splits = cuaddr.split(":")[0][2:].split("?")
- domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
- if splits[0].endswith(domain):
- return True
-
- elif cuaddr.startswith("/"):
- # Assume relative HTTP URL - i.e. on this server
- return True
-
- # Do default match
- return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
-
- @inlineCallbacks
- def generateSchedulingResponses(self):
-
- # Extract the ORGANIZER property and UID value from the calendar data for use later
- organizerProp = self.scheduler.calendar.getOrganizerProperty()
- uid = self.scheduler.calendar.resourceUID()
-
- organizerPrincipal = None
- if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser,):
- organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
-
- for recipient in self.recipients:
-
- #
- # Check access controls
- #
- if organizerPrincipal:
- try:
- yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.Schedule(),), principal=organizerPrincipal)
- except AccessDeniedError:
- log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
-
- # Process next recipient
- continue
- else:
- # TODO: need to figure out how best to do server-to-server authorization.
- # First thing would be to check for DAV:unauthenticated privilege.
- # Next would be to allow the calendar user address of the organizer/originator to be used
- # as a principal.
- pass
-
- # Different behavior for free-busy vs regular invite
- if self.freebusy:
- yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid)
- else:
- yield self.generateResponse(recipient, self.responses)
-
- @inlineCallbacks
- def generateResponse(self, recipient, responses):
- # Hash the iCalendar data for use as the last path element of the URI path
- calendar_str = self.scheduler.calendardata
- name = md5(calendar_str + str(time.time()) + recipient.inbox.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- childURL = joinURL(recipient.inboxURL, name)
- child = (yield self.scheduler.request.locateResource(childURL))
-
- try:
- # Copy calendar to inbox (doing fan-out)
- from twistedcaldav.method.put_common import storeCalendarObjectResource
- yield storeCalendarObjectResource(
- request = self.scheduler.request,
- sourcecal = False,
- destination = child,
- destination_uri = childURL,
- calendardata = self.scheduler.calendardata,
- destinationparent = recipient.inbox,
- destinationcal = True,
- isiTIP = True
- )
- except: # FIXME: bare except
- self.log_error("Could not store data in Inbox : %s" % (recipient.inbox,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
- returnValue(False)
- else:
- responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
-
- # Store CALDAV:originator property
- child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
-
- # Store CALDAV:recipient property
- child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
-
- # Look for auto-schedule option
- if recipient.principal.autoSchedule():
- if not hasattr(self.scheduler.request, "extendedLogItems"):
- self.scheduler.request.extendedLogItems = {}
- self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
-
- # Add delayed reactor task to handle iTIP responses
- reactor.callLater(0.0, handleRequest, *(self.scheduler.request, recipient.principal, recipient.inbox, self.scheduler.calendar.duplicate(), child)) #@UndefinedVariable
-
- @inlineCallbacks
- def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid):
-
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = recipient.principal.calendarUserAddresses()
- attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
-
- remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
- try:
- fbresult = (yield self.generateAttendeeFreeBusyResponse(
- recipient,
- organizerProp,
- organizerPrincipal,
- uid,
- attendeeProp,
- remote,
- ))
- except:
- log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
- responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
- returnValue(False)
- else:
- responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult)
- returnValue(True)
-
- @inlineCallbacks
- def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote):
-
- # Find the current recipients calendar-free-busy-set
- fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- # Process the availability property from the Inbox.
- has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
- if has_prop:
- availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
- availability = availability.calendar()
- report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
- # Check to see if the recipient is the same calendar user as the organizer.
- # Needed for masked UID stuff.
- if isinstance(self.scheduler.organizer, LocalCalendarUser):
- same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
- else:
- same_calendar_user = False
-
- # Now process free-busy set calendars
- matchtotal = 0
- for calendarResourceURL in fbset:
-
- # Yield to the reactor once through each loop
- d = Deferred()
- def _timedDeferred():
- d.callback(True)
- reactor.callLater(0.0, _timedDeferred)
- yield d
-
- calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
- if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
-
- matchtotal = (yield report_common.generateFreeBusyInfo(
- self.scheduler.request,
- calendarResource,
- fbinfo,
- self.scheduler.timeRange,
- matchtotal,
- excludeuid = self.scheduler.excludeUID,
- organizer = self.scheduler.organizer.cuaddr,
- organizerPrincipal = organizerPrincipal,
- same_calendar_user = same_calendar_user,
- ))
-
- # Build VFREEBUSY iTIP reply for this recipient
- fbresult = report_common.buildFreeBusyResult(
- fbinfo,
- self.scheduler.timeRange,
- organizer = organizerProp,
- attendee = attendeeProp,
- uid = uid,
- method = "REPLY"
- )
-
- returnValue(fbresult)
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/caldav.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/caldav.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,267 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.extensions import ErrorResponse
+
+import time
+
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import new as md5
+
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.log import Logger
+from twistedcaldav.method import report_common
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser,\
+ PartitionedCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.scheduling.itip import handleRequest
+
+"""
+Class that handles delivery of scheduling messages via CalDAV.
+"""
+
+__all__ = [
+ "ScheduleViaCalDAV",
+]
+
+log = Logger()
+
+class ScheduleViaCalDAV(DeliveryService):
+
+ def __init__(self, scheduler, recipients, responses, freebusy):
+
+ self.scheduler = scheduler
+ self.recipients = recipients
+ self.responses = responses
+ self.freebusy = freebusy
+
+ @classmethod
+ def serviceType(cls):
+ return DeliveryService.serviceType_caldav
+
+ @classmethod
+ def matchCalendarUserAddress(cls, cuaddr):
+
+ # Check for local address matches first
+ if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
+ splits = cuaddr[7:].split("?")
+ domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
+ if splits[0].endswith(domain):
+ return True
+
+ elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
+ splits = cuaddr.split(":")[0][2:].split("?")
+ domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
+ if splits[0].endswith(domain):
+ return True
+
+ elif cuaddr.startswith("/"):
+ # Assume relative HTTP URL - i.e. on this server
+ return True
+
+ # Do default match
+ return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
+
+ @inlineCallbacks
+ def generateSchedulingResponses(self):
+
+ # Extract the ORGANIZER property and UID value from the calendar data for use later
+ organizerProp = self.scheduler.calendar.getOrganizerProperty()
+ uid = self.scheduler.calendar.resourceUID()
+
+ organizerPrincipal = None
+ if type(self.scheduler.organizer) in (LocalCalendarUser, PartitionedCalendarUser,):
+ organizerPrincipal = davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL()))
+
+ for recipient in self.recipients:
+
+ #
+ # Check access controls
+ #
+ if organizerPrincipal:
+ try:
+ yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.Schedule(),), principal=organizerPrincipal)
+ except AccessDeniedError:
+ log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
+
+ # Process next recipient
+ continue
+ else:
+ # TODO: need to figure out how best to do server-to-server authorization.
+ # First thing would be to check for DAV:unauthenticated privilege.
+ # Next would be to allow the calendar user address of the organizer/originator to be used
+ # as a principal.
+ pass
+
+ # Different behavior for free-busy vs regular invite
+ if self.freebusy:
+ yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid)
+ else:
+ yield self.generateResponse(recipient, self.responses)
+
+ @inlineCallbacks
+ def generateResponse(self, recipient, responses):
+ # Hash the iCalendar data for use as the last path element of the URI path
+ calendar_str = self.scheduler.calendardata
+ name = md5(calendar_str + str(time.time()) + recipient.inbox.fp.path).hexdigest() + ".ics"
+
+ # Get a resource for the new item
+ childURL = joinURL(recipient.inboxURL, name)
+ child = (yield self.scheduler.request.locateResource(childURL))
+
+ try:
+ # Copy calendar to inbox (doing fan-out)
+ from twistedcaldav.method.put_common import storeCalendarObjectResource
+ yield storeCalendarObjectResource(
+ request = self.scheduler.request,
+ sourcecal = False,
+ destination = child,
+ destination_uri = childURL,
+ calendardata = self.scheduler.calendardata,
+ destinationparent = recipient.inbox,
+ destinationcal = True,
+ isiTIP = True
+ )
+ except: # FIXME: bare except
+ self.log_error("Could not store data in Inbox : %s" % (recipient.inbox,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+ returnValue(False)
+ else:
+ responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+
+ # Store CALDAV:originator property
+ child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.originator.cuaddr)))
+
+ # Store CALDAV:recipient property
+ child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
+
+ # Look for auto-schedule option
+ if recipient.principal.autoSchedule():
+ if not hasattr(self.scheduler.request, "extendedLogItems"):
+ self.scheduler.request.extendedLogItems = {}
+ self.scheduler.request.extendedLogItems["itip.auto"] = self.scheduler.request.extendedLogItems.get("itip.auto", 0) + 1
+
+ # Add delayed reactor task to handle iTIP responses
+ reactor.callLater(0.0, handleRequest, *(self.scheduler.request, recipient.principal, recipient.inbox, self.scheduler.calendar.duplicate(), child)) #@UndefinedVariable
+
+ @inlineCallbacks
+ def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid):
+
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = recipient.principal.calendarUserAddresses()
+ attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
+
+ remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
+
+ try:
+ fbresult = (yield self.generateAttendeeFreeBusyResponse(
+ recipient,
+ organizerProp,
+ organizerPrincipal,
+ uid,
+ attendeeProp,
+ remote,
+ ))
+ except:
+ log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_AUTHORITY)
+ returnValue(False)
+ else:
+ responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS, calendar=fbresult)
+ returnValue(True)
+
+ @inlineCallbacks
+ def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote):
+
+ # Find the current recipients calendar-free-busy-set
+ fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
+
+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = ([], [], [])
+
+ # Process the availability property from the Inbox.
+ has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+ if has_prop:
+ availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+ availability = availability.calendar()
+ report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
+
+ # Check to see if the recipient is the same calendar user as the organizer.
+ # Needed for masked UID stuff.
+ if isinstance(self.scheduler.organizer, LocalCalendarUser):
+ same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
+ else:
+ same_calendar_user = False
+
+ # Now process free-busy set calendars
+ matchtotal = 0
+ for calendarResourceURL in fbset:
+
+ # Yield to the reactor once through each loop
+ d = Deferred()
+ def _timedDeferred():
+ d.callback(True)
+ reactor.callLater(0.0, _timedDeferred)
+ yield d
+
+ calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
+ if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
+ # We will ignore missing calendars. If the recipient has failed to
+ # properly manage the free busy set that should not prevent us from working.
+ continue
+
+ matchtotal = (yield report_common.generateFreeBusyInfo(
+ self.scheduler.request,
+ calendarResource,
+ fbinfo,
+ self.scheduler.timeRange,
+ matchtotal,
+ excludeuid = self.scheduler.excludeUID,
+ organizer = self.scheduler.organizer.cuaddr,
+ organizerPrincipal = organizerPrincipal,
+ same_calendar_user = same_calendar_user,
+ ))
+
+ # Build VFREEBUSY iTIP reply for this recipient
+ fbresult = report_common.buildFreeBusyResult(
+ fbinfo,
+ self.scheduler.timeRange,
+ organizer = organizerProp,
+ attendee = attendeeProp,
+ uid = uid,
+ method = "REPLY"
+ )
+
+ returnValue(fbresult)
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/cuaddress.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,94 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.delivery import DeliveryService
-
-__all__ = [
- "LocalCalendarUser",
- "RemoteCalendarUser",
- "InvalidCalendarUser",
- "normalizeCUAddr",
-]
-
-log = Logger()
-
-class CalendarUser(object):
- def __init__(self, cuaddr):
- self.cuaddr = cuaddr
- self.serviceType = None
-
-class LocalCalendarUser(CalendarUser):
- def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
- self.cuaddr = cuaddr
- self.principal = principal
- self.inbox = inbox
- self.inboxURL = inboxURL
- self.serviceType = DeliveryService.serviceType_caldav
-
- def __str__(self):
- return "Local calendar user: %s" % (self.cuaddr,)
-
-class PartitionedCalendarUser(CalendarUser):
- def __init__(self, cuaddr, principal):
- self.cuaddr = cuaddr
- self.principal = principal
- self.serviceType = DeliveryService.serviceType_ischedule
-
- def __str__(self):
- return "Partitioned calendar user: %s" % (self.cuaddr,)
-
-class RemoteCalendarUser(CalendarUser):
- def __init__(self, cuaddr):
- self.cuaddr = cuaddr
- self.extractDomain()
- self.serviceType = DeliveryService.serviceType_ischedule
-
- def __str__(self):
- return "Remote calendar user: %s" % (self.cuaddr,)
-
- def extractDomain(self):
- if self.cuaddr.startswith("mailto:"):
- splits = self.cuaddr[7:].split("?")
- self.domain = splits[0].split("@")[1]
- elif self.cuaddr.startswith("http://") or self.cuaddr.startswith("https://"):
- splits = self.cuaddr.split(":")[1][2:].split("/")
- self.domain = splits[0]
- else:
- self.domain = ""
-
-class InvalidCalendarUser(CalendarUser):
-
- def __str__(self):
- return "Invalid calendar user: %s" % (self.cuaddr,)
-
-
-def normalizeCUAddr(addr):
- """
- Normalize a cuaddr string by lower()ing it if it's a mailto:, or
- removing trailing slash if it's a URL.
- @param addr: a cuaddr string to normalize
- @return: normalized string
- """
- lower = addr.lower()
- if lower.startswith("mailto:"):
- addr = lower
- if (addr.startswith("/") or
- addr.startswith("http:") or
- addr.startswith("https:")):
- return addr.rstrip("/")
- else:
- return addr
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/cuaddress.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/cuaddress.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,94 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+__all__ = [
+ "LocalCalendarUser",
+ "RemoteCalendarUser",
+ "InvalidCalendarUser",
+ "normalizeCUAddr",
+]
+
+log = Logger()
+
+class CalendarUser(object):
+ def __init__(self, cuaddr):
+ self.cuaddr = cuaddr
+ self.serviceType = None
+
+class LocalCalendarUser(CalendarUser):
+ def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
+ self.cuaddr = cuaddr
+ self.principal = principal
+ self.inbox = inbox
+ self.inboxURL = inboxURL
+ self.serviceType = DeliveryService.serviceType_caldav
+
+ def __str__(self):
+ return "Local calendar user: %s" % (self.cuaddr,)
+
+class PartitionedCalendarUser(CalendarUser):
+ def __init__(self, cuaddr, principal):
+ self.cuaddr = cuaddr
+ self.principal = principal
+ self.serviceType = DeliveryService.serviceType_ischedule
+
+ def __str__(self):
+ return "Partitioned calendar user: %s" % (self.cuaddr,)
+
+class RemoteCalendarUser(CalendarUser):
+ def __init__(self, cuaddr):
+ self.cuaddr = cuaddr
+ self.extractDomain()
+ self.serviceType = DeliveryService.serviceType_ischedule
+
+ def __str__(self):
+ return "Remote calendar user: %s" % (self.cuaddr,)
+
+ def extractDomain(self):
+ if self.cuaddr.startswith("mailto:"):
+ splits = self.cuaddr[7:].split("?")
+ self.domain = splits[0].split("@")[1]
+ elif self.cuaddr.startswith("http://") or self.cuaddr.startswith("https://"):
+ splits = self.cuaddr.split(":")[1][2:].split("/")
+ self.domain = splits[0]
+ else:
+ self.domain = ""
+
+class InvalidCalendarUser(CalendarUser):
+
+ def __str__(self):
+ return "Invalid calendar user: %s" % (self.cuaddr,)
+
+
+def normalizeCUAddr(addr):
+ """
+ Normalize a cuaddr string by lower()ing it if it's a mailto:, or
+ removing trailing slash if it's a URL.
+ @param addr: a cuaddr string to normalize
+ @return: normalized string
+ """
+ lower = addr.lower()
+ if lower.startswith("mailto:"):
+ addr = lower
+ if (addr.startswith("/") or
+ addr.startswith("http:") or
+ addr.startswith("https:")):
+ return addr.rstrip("/")
+ else:
+ return addr
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/delivery.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,63 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.config import config
-from twistedcaldav.log import Logger
-
-import re
-
-__all__ = [
- "DeliveryService",
-]
-
-log = Logger()
-
-class DeliveryService(object):
- """
- Abstract base class that defines a delivery method for a scheduling message.
- """
-
- # Known types
-
- serviceType_caldav = 'CalDAV'
- serviceType_ischedule = 'iSchedule'
-
- def __init__(self, scheduler, recipients, responses, freebusy):
-
- self.scheduler = scheduler
- self.recipients = recipients
- self.responses = responses
- self.freebusy = freebusy
-
- @classmethod
- def serviceType(cls):
- raise NotImplementedError
-
- @classmethod
- def matchCalendarUserAddress(cls, cuaddr):
-
- # Do the pattern match
- for pattern in config.Scheduling[cls.serviceType()]["AddressPatterns"]:
- try:
- if re.match(pattern, cuaddr) is not None:
- return True
- except re.error:
- log.error("Invalid regular expression for Scheduling configuration '%s/LocalAddresses': %s" % (cls.serviceType(), pattern,))
-
- return False
-
- def generateSchedulingResponses(self):
- raise NotImplementedError
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/delivery.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/delivery.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,63 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+
+import re
+
+__all__ = [
+ "DeliveryService",
+]
+
+log = Logger()
+
+class DeliveryService(object):
+ """
+ Abstract base class that defines a delivery method for a scheduling message.
+ """
+
+ # Known types
+
+ serviceType_caldav = 'CalDAV'
+ serviceType_ischedule = 'iSchedule'
+
+ def __init__(self, scheduler, recipients, responses, freebusy):
+
+ self.scheduler = scheduler
+ self.recipients = recipients
+ self.responses = responses
+ self.freebusy = freebusy
+
+ @classmethod
+ def serviceType(cls):
+ raise NotImplementedError
+
+ @classmethod
+ def matchCalendarUserAddress(cls, cuaddr):
+
+ # Do the pattern match
+ for pattern in config.Scheduling[cls.serviceType()]["AddressPatterns"]:
+ try:
+ if re.match(pattern, cuaddr) is not None:
+ return True
+ except re.error:
+ log.error("Invalid regular expression for Scheduling configuration '%s/LocalAddresses': %s" % (cls.serviceType(), pattern,))
+
+ return False
+
+ def generateSchedulingResponses(self):
+ raise NotImplementedError
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischedule.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,224 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, DeferredList
-from twisted.internet.protocol import ClientCreator
-
-from twisted.python.failure import Failure
-
-from twisted.web2 import responsecode
-from twisted.web2.client.http import ClientRequest
-from twisted.web2.client.http import HTTPClientProtocol
-from twisted.web2.dav.util import davXMLFromStream, joinURL
-from twisted.web2.http import HTTPError
-from twisted.web2.http_headers import Headers
-from twisted.web2.http_headers import MimeType
-
-from twisted.internet.ssl import DefaultOpenSSLContextFactory
-
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.config import config
-from twistedcaldav.extensions import ErrorResponse
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.delivery import DeliveryService
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers,\
- IScheduleServerRecord
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-from twistedcaldav.util import utf8String
-from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser,\
- PartitionedCalendarUser
-
-import OpenSSL
-
-"""
-Server to server utility functions and client requests.
-"""
-
-__all__ = [
- "ScheduleViaISchedule",
-]
-
-log = Logger()
-
-class ScheduleViaISchedule(DeliveryService):
-
- @classmethod
- def serviceType(cls):
- return DeliveryService.serviceType_ischedule
-
- @classmethod
- def matchCalendarUserAddress(cls, cuaddr):
-
- # TODO: here is where we would attempt service discovery based on the cuaddr.
-
- # Do default match
- return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
-
- def generateSchedulingResponses(self):
- """
- Generate scheduling responses for remote recipients.
- """
-
- # Group recipients by server so that we can do a single request with multiple recipients
- # to each different server.
- groups = {}
- servermgr = IScheduleServers()
- for recipient in self.recipients:
- if isinstance(recipient, RemoteCalendarUser):
- # Map the recipient's domain to a server
- server = servermgr.mapDomain(recipient.domain)
- elif isinstance(recipient, PartitionedCalendarUser):
- server = self._getServerForPartitionedUser(recipient)
- else:
- assert False, "Incorrect calendar user address class"
- if not server:
- # Cannot do server-to-server for this recipient.
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
-
- # Process next recipient
- continue
-
- if not server.allow_to:
- # Cannot do server-to-server outgoing requests for this server.
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- # Process next recipient
- continue
-
- groups.setdefault(server, []).append(recipient)
-
- if len(groups) == 0:
- return
-
- # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
- # we will generate for each request. That way we can have parallel requests in progress
- # rather than serialize them.
- deferreds = []
- for server, recipients in groups.iteritems():
- requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses)
- deferreds.append(requestor.doRequest())
-
- return DeferredList(deferreds)
-
- def _getServerForPartitionedUser(self, recipient):
-
- if not hasattr(self, "partitionedServers"):
- self.partitionedServers = {}
-
- partition = recipient.principal.hostedURL()
- if partition not in self.partitionedServers:
- self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
- self.partitionedServers[partition].unNormalizeAddresses = False
-
- return self.partitionedServers[partition]
-
-class IScheduleRequest(object):
-
- def __init__(self, scheduler, server, recipients, responses):
-
- self.scheduler = scheduler
- self.server = server
- self.recipients = recipients
- self.responses = responses
-
- self._generateHeaders()
- self._prepareData()
-
- @inlineCallbacks
- def doRequest(self):
-
- # Generate an HTTP client request
- try:
- from twisted.internet import reactor
- if self.server.ssl:
- context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
- proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
- else:
- proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
-
- request = ClientRequest("POST", self.server.path, self.headers, self.data)
- yield log.logRequest("debug", "Sending server-to-server POST request:", request)
- response = (yield proto.submitRequest(request))
-
- yield log.logResponse("debug", "Received server-to-server POST response:", response)
- xml = (yield davXMLFromStream(response.stream))
-
- self._parseResponse(xml)
-
- except Exception, e:
- # Generated failed responses for each recipient
- log.err("Could not do server-to-server request : %s %s" % (self, e))
- for recipient in self.recipients:
- err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
- self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
-
- def _generateHeaders(self):
- self.headers = Headers()
- self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
-
- # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
- self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
- self._doAuthentication()
- for recipient in self.recipients:
- self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
- self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
-
- def _doAuthentication(self):
- if self.server.authentication and self.server.authentication[0] == "basic":
- self.headers.setHeader(
- 'Authorization',
- ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
- )
-
- def _prepareData(self):
- if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
- def lookupFunction(cuaddr):
- principal = self.scheduler.resource.principalForCalendarUserAddress(cuaddr)
- if principal is None:
- return (None, None, None)
- else:
- return (principal.record.fullName.decode("utf-8"),
- principal.record.guid,
- principal.record.calendarUserAddresses)
-
- normalizedCalendar = self.scheduler.calendar.duplicate()
- normalizedCalendar.normalizeCalendarUserAddresses(lookupFunction, toUUID=False)
- else:
- normalizedCalendar = self.scheduler.calendar
- self.data = str(normalizedCalendar)
-
- def _parseResponse(self, xml):
-
- # Check for correct root element
- schedule_response = xml.root_element
- if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
- raise HTTPError(responsecode.BAD_REQUEST)
-
- # Parse each response - do this twice: once looking for errors that will
- # result in all recipients shown as failures; the second loop adds all the
- # valid responses to the actual result.
- for response in schedule_response.children:
- if not isinstance(response, caldavxml.Response) or not response.children:
- raise HTTPError(responsecode.BAD_REQUEST)
- recipient = response.childOfType(caldavxml.Recipient)
- request_status = response.childOfType(caldavxml.RequestStatus)
- if not recipient or not request_status:
- raise HTTPError(responsecode.BAD_REQUEST)
- for response in schedule_response.children:
- self.responses.clone(response)
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischedule.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischedule.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,224 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, DeferredList
+from twisted.internet.protocol import ClientCreator
+
+from twisted.python.failure import Failure
+
+from twisted.web2 import responsecode
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.dav.util import davXMLFromStream, joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http_headers import Headers
+from twisted.web2.http_headers import MimeType
+
+from twisted.internet.ssl import DefaultOpenSSLContextFactory
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.extensions import ErrorResponse
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers,\
+ IScheduleServerRecord
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+from twistedcaldav.util import utf8String
+from twistedcaldav.scheduling.cuaddress import RemoteCalendarUser,\
+ PartitionedCalendarUser
+
+import OpenSSL
+
+"""
+Server to server utility functions and client requests.
+"""
+
+__all__ = [
+ "ScheduleViaISchedule",
+]
+
+log = Logger()
+
+class ScheduleViaISchedule(DeliveryService):
+
+ @classmethod
+ def serviceType(cls):
+ return DeliveryService.serviceType_ischedule
+
+ @classmethod
+ def matchCalendarUserAddress(cls, cuaddr):
+
+ # TODO: here is where we would attempt service discovery based on the cuaddr.
+
+ # Do default match
+ return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
+
+ def generateSchedulingResponses(self):
+ """
+ Generate scheduling responses for remote recipients.
+ """
+
+ # Group recipients by server so that we can do a single request with multiple recipients
+ # to each different server.
+ groups = {}
+ servermgr = IScheduleServers()
+ for recipient in self.recipients:
+ if isinstance(recipient, RemoteCalendarUser):
+ # Map the recipient's domain to a server
+ server = servermgr.mapDomain(recipient.domain)
+ elif isinstance(recipient, PartitionedCalendarUser):
+ server = self._getServerForPartitionedUser(recipient)
+ else:
+ assert False, "Incorrect calendar user address class"
+ if not server:
+ # Cannot do server-to-server for this recipient.
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.NO_USER_SUPPORT)
+
+ # Process next recipient
+ continue
+
+ if not server.allow_to:
+ # Cannot do server-to-server outgoing requests for this server.
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+ # Process next recipient
+ continue
+
+ groups.setdefault(server, []).append(recipient)
+
+ if len(groups) == 0:
+ return
+
+ # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+ # we will generate for each request. That way we can have parallel requests in progress
+ # rather than serialize them.
+ deferreds = []
+ for server, recipients in groups.iteritems():
+ requestor = IScheduleRequest(self.scheduler, server, recipients, self.responses)
+ deferreds.append(requestor.doRequest())
+
+ return DeferredList(deferreds)
+
+ def _getServerForPartitionedUser(self, recipient):
+
+ if not hasattr(self, "partitionedServers"):
+ self.partitionedServers = {}
+
+ partition = recipient.principal.hostedURL()
+ if partition not in self.partitionedServers:
+ self.partitionedServers[partition] = IScheduleServerRecord(uri=joinURL(partition, "/ischedule"))
+ self.partitionedServers[partition].unNormalizeAddresses = False
+
+ return self.partitionedServers[partition]
+
+class IScheduleRequest(object):
+
+ def __init__(self, scheduler, server, recipients, responses):
+
+ self.scheduler = scheduler
+ self.server = server
+ self.recipients = recipients
+ self.responses = responses
+
+ self._generateHeaders()
+ self._prepareData()
+
+ @inlineCallbacks
+ def doRequest(self):
+
+ # Generate an HTTP client request
+ try:
+ from twisted.internet import reactor
+ if self.server.ssl:
+ context = DefaultOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, sslmethod=OpenSSL.SSL.SSLv3_METHOD)
+ proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
+ else:
+ proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
+
+ request = ClientRequest("POST", self.server.path, self.headers, self.data)
+ yield log.logRequest("debug", "Sending server-to-server POST request:", request)
+ response = (yield proto.submitRequest(request))
+
+ yield log.logResponse("debug", "Received server-to-server POST response:", response)
+ xml = (yield davXMLFromStream(response.stream))
+
+ self._parseResponse(xml)
+
+ except Exception, e:
+ # Generated failed responses for each recipient
+ log.err("Could not do server-to-server request : %s %s" % (self, e))
+ for recipient in self.recipients:
+ err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
+ self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.SERVICE_UNAVAILABLE)
+
+ def _generateHeaders(self):
+ self.headers = Headers()
+ self.headers.setHeader('Host', utf8String(self.server.host + ":%s" % (self.server.port,)))
+
+ # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
+ self.headers.addRawHeader('Originator', utf8String(self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee))
+ self._doAuthentication()
+ for recipient in self.recipients:
+ self.headers.addRawHeader('Recipient', utf8String(recipient.cuaddr))
+ self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
+
+ def _doAuthentication(self):
+ if self.server.authentication and self.server.authentication[0] == "basic":
+ self.headers.setHeader(
+ 'Authorization',
+ ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+ )
+
+ def _prepareData(self):
+ if self.server.unNormalizeAddresses and self.scheduler.method == "PUT":
+ def lookupFunction(cuaddr):
+ principal = self.scheduler.resource.principalForCalendarUserAddress(cuaddr)
+ if principal is None:
+ return (None, None, None)
+ else:
+ return (principal.record.fullName.decode("utf-8"),
+ principal.record.guid,
+ principal.record.calendarUserAddresses)
+
+ normalizedCalendar = self.scheduler.calendar.duplicate()
+ normalizedCalendar.normalizeCalendarUserAddresses(lookupFunction, toUUID=False)
+ else:
+ normalizedCalendar = self.scheduler.calendar
+ self.data = str(normalizedCalendar)
+
+ def _parseResponse(self, xml):
+
+ # Check for correct root element
+ schedule_response = xml.root_element
+ if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
+ raise HTTPError(responsecode.BAD_REQUEST)
+
+ # Parse each response - do this twice: once looking for errors that will
+ # result in all recipients shown as failures; the second loop adds all the
+ # valid responses to the actual result.
+ for response in schedule_response.children:
+ if not isinstance(response, caldavxml.Response) or not response.children:
+ raise HTTPError(responsecode.BAD_REQUEST)
+ recipient = response.childOfType(caldavxml.Recipient)
+ request_status = response.childOfType(caldavxml.RequestStatus)
+ if not recipient or not request_status:
+ raise HTTPError(responsecode.BAD_REQUEST)
+ for response in schedule_response.children:
+ self.responses.clone(response)
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischeduleservers.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,207 +0,0 @@
-##
-# Copyright (c) 2006-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.config import config
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.delivery import DeliveryService
-
-import xml.dom.minidom
-
-"""
-XML based iSchedule configuration file handling.
-"""
-
-__all__ = [
- "IScheduleServers",
-]
-
-log = Logger()
-
-class IScheduleServers(object):
-
- _fileInfo = None
- _xmlFile = None
- _servers = None
- _domainMap = None
-
- def __init__(self):
-
- self._loadConfig()
-
- def _loadConfig(self):
- if IScheduleServers._servers is None:
- IScheduleServers._xmlFile = FilePath(config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"])
- IScheduleServers._xmlFile.restat()
- fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
- if fileInfo != IScheduleServers._fileInfo:
- parser = IScheduleServersParser(IScheduleServers._xmlFile)
- IScheduleServers._servers = parser.servers
- self._mapDomains()
- IScheduleServers._fileInfo = fileInfo
-
- def _mapDomains(self):
- IScheduleServers._domainMap = {}
- for server in IScheduleServers._servers:
- for domain in server.domains:
- IScheduleServers._domainMap[domain] = server
-
- def mapDomain(self, domain):
- """
- Map a calendar user address domain to a suitable server that can
- handle server-to-server requests for that user.
- """
- return IScheduleServers._domainMap.get(domain)
-
-ELEMENT_SERVERS = "servers"
-ELEMENT_SERVER = "server"
-ELEMENT_URI = "uri"
-ELEMENT_AUTHENTICATION = "authentication"
-ATTRIBUTE_TYPE = "type"
-ATTRIBUTE_BASICAUTH = "basic"
-ELEMENT_USER = "user"
-ELEMENT_PASSWORD = "password"
-ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
-ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
-ELEMENT_DOMAINS = "domains"
-ELEMENT_DOMAIN = "domain"
-ELEMENT_CLIENT_HOSTS = "hosts"
-ELEMENT_HOST = "host"
-
-class IScheduleServersParser(object):
- """
- Server-to-server configuration file parser.
- """
- def __repr__(self):
- return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
- def __init__(self, xmlFile):
-
- self.servers = []
-
- # Read in XML
- fd = open(xmlFile.path, "r")
- doc = xml.dom.minidom.parse(fd)
- fd.close()
-
- # Verify that top-level element is correct
- servers_node = doc._get_documentElement()
- if servers_node._get_localName() != ELEMENT_SERVERS:
- log.error("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
- return
- self._parseXML(servers_node)
-
- def _parseXML(self, node):
- """
- Parse the XML root node from the server-to-server configuration document.
- @param node: the L{Node} to parse.
- """
-
- for child in node._get_childNodes():
- child_name = child._get_localName()
- if child_name is None:
- continue
- elif child_name == ELEMENT_SERVER:
- self.servers.append(IScheduleServerRecord())
- self.servers[-1].parseXML(child)
-
-class IScheduleServerRecord (object):
- """
- Contains server-to-server details.
- """
- def __init__(self, uri=None):
- """
- @param recordType: record type for directory entry.
- """
- self.uri = ""
- self.authentication = None
- self.allow_from = False
- self.allow_to = True
- self.domains = []
- self.client_hosts = []
- self.unNormalizeAddresses = True
-
- if uri:
- self.uri = uri
- self._parseDetails()
-
- def parseXML(self, node):
- for child in node._get_childNodes():
- child_name = child._get_localName()
- if child_name is None:
- continue
- elif child_name == ELEMENT_URI:
- if child.firstChild is not None:
- self.uri = child.firstChild.data.encode("utf-8")
- elif child_name == ELEMENT_AUTHENTICATION:
- self._parseAuthentication(child)
- elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
- self.allow_from = True
- elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
- self.allow_to = True
- elif child_name == ELEMENT_DOMAINS:
- self._parseList(child, ELEMENT_DOMAIN, self.domains)
- elif child_name == ELEMENT_CLIENT_HOSTS:
- self._parseList(child, ELEMENT_HOST, self.client_hosts)
- else:
- raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
-
- self._parseDetails()
-
- def _parseList(self, node, element_name, appendto):
- for child in node._get_childNodes():
- if child._get_localName() == element_name:
- if child.firstChild is not None:
- appendto.append(child.firstChild.data.encode("utf-8"))
-
- def _parseAuthentication(self, node):
- if node.hasAttribute(ATTRIBUTE_TYPE):
- atype = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
- if atype != ATTRIBUTE_BASICAUTH:
- return
- else:
- return
-
- for child in node._get_childNodes():
- if child._get_localName() == ELEMENT_USER:
- if child.firstChild is not None:
- user = child.firstChild.data.encode("utf-8")
- elif child._get_localName() == ELEMENT_PASSWORD:
- if child.firstChild is not None:
- password = child.firstChild.data.encode("utf-8")
-
- self.authentication = ("basic", user, password,)
-
- def _parseDetails(self):
- # Extract scheme, host, port and path
- if self.uri.startswith("http://"):
- self.ssl = False
- rest = self.uri[7:]
- elif self.uri.startswith("https://"):
- self.ssl = True
- rest = self.uri[8:]
-
- splits = rest.split("/", 1)
- hostport = splits[0].split(":")
- self.host = hostport[0]
- if len(hostport) > 1:
- self.port = int(hostport[1])
- else:
- self.port = {False:80, True:443}[self.ssl]
- self.path = "/"
- if len(splits) > 1:
- self.path += splits[1]
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/ischeduleservers.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/ischeduleservers.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,207 @@
+##
+# Copyright (c) 2006-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.python.filepath import FilePath
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+import xml.dom.minidom
+
+"""
+XML based iSchedule configuration file handling.
+"""
+
+__all__ = [
+ "IScheduleServers",
+]
+
+log = Logger()
+
+class IScheduleServers(object):
+
+ _fileInfo = None
+ _xmlFile = None
+ _servers = None
+ _domainMap = None
+
+ def __init__(self):
+
+ self._loadConfig()
+
+ def _loadConfig(self):
+ if IScheduleServers._servers is None:
+ IScheduleServers._xmlFile = FilePath(config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"])
+ IScheduleServers._xmlFile.restat()
+ fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+ if fileInfo != IScheduleServers._fileInfo:
+ parser = IScheduleServersParser(IScheduleServers._xmlFile)
+ IScheduleServers._servers = parser.servers
+ self._mapDomains()
+ IScheduleServers._fileInfo = fileInfo
+
+ def _mapDomains(self):
+ IScheduleServers._domainMap = {}
+ for server in IScheduleServers._servers:
+ for domain in server.domains:
+ IScheduleServers._domainMap[domain] = server
+
+ def mapDomain(self, domain):
+ """
+ Map a calendar user address domain to a suitable server that can
+ handle server-to-server requests for that user.
+ """
+ return IScheduleServers._domainMap.get(domain)
+
+ELEMENT_SERVERS = "servers"
+ELEMENT_SERVER = "server"
+ELEMENT_URI = "uri"
+ELEMENT_AUTHENTICATION = "authentication"
+ATTRIBUTE_TYPE = "type"
+ATTRIBUTE_BASICAUTH = "basic"
+ELEMENT_USER = "user"
+ELEMENT_PASSWORD = "password"
+ELEMENT_ALLOW_REQUESTS_FROM = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO = "allow-requests-to"
+ELEMENT_DOMAINS = "domains"
+ELEMENT_DOMAIN = "domain"
+ELEMENT_CLIENT_HOSTS = "hosts"
+ELEMENT_HOST = "host"
+
+class IScheduleServersParser(object):
+ """
+ Server-to-server configuration file parser.
+ """
+ def __repr__(self):
+ return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+ def __init__(self, xmlFile):
+
+ self.servers = []
+
+ # Read in XML
+ fd = open(xmlFile.path, "r")
+ doc = xml.dom.minidom.parse(fd)
+ fd.close()
+
+ # Verify that top-level element is correct
+ servers_node = doc._get_documentElement()
+ if servers_node._get_localName() != ELEMENT_SERVERS:
+ log.error("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
+ return
+ self._parseXML(servers_node)
+
+ def _parseXML(self, node):
+ """
+ Parse the XML root node from the server-to-server configuration document.
+ @param node: the L{Node} to parse.
+ """
+
+ for child in node._get_childNodes():
+ child_name = child._get_localName()
+ if child_name is None:
+ continue
+ elif child_name == ELEMENT_SERVER:
+ self.servers.append(IScheduleServerRecord())
+ self.servers[-1].parseXML(child)
+
+class IScheduleServerRecord (object):
+ """
+ Contains server-to-server details.
+ """
+ def __init__(self, uri=None):
+ """
+ @param recordType: record type for directory entry.
+ """
+ self.uri = ""
+ self.authentication = None
+ self.allow_from = False
+ self.allow_to = True
+ self.domains = []
+ self.client_hosts = []
+ self.unNormalizeAddresses = True
+
+ if uri:
+ self.uri = uri
+ self._parseDetails()
+
+ def parseXML(self, node):
+ for child in node._get_childNodes():
+ child_name = child._get_localName()
+ if child_name is None:
+ continue
+ elif child_name == ELEMENT_URI:
+ if child.firstChild is not None:
+ self.uri = child.firstChild.data.encode("utf-8")
+ elif child_name == ELEMENT_AUTHENTICATION:
+ self._parseAuthentication(child)
+ elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
+ self.allow_from = True
+ elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
+ self.allow_to = True
+ elif child_name == ELEMENT_DOMAINS:
+ self._parseList(child, ELEMENT_DOMAIN, self.domains)
+ elif child_name == ELEMENT_CLIENT_HOSTS:
+ self._parseList(child, ELEMENT_HOST, self.client_hosts)
+ else:
+ raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
+
+ self._parseDetails()
+
+ def _parseList(self, node, element_name, appendto):
+ for child in node._get_childNodes():
+ if child._get_localName() == element_name:
+ if child.firstChild is not None:
+ appendto.append(child.firstChild.data.encode("utf-8"))
+
+ def _parseAuthentication(self, node):
+ if node.hasAttribute(ATTRIBUTE_TYPE):
+ atype = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
+ if atype != ATTRIBUTE_BASICAUTH:
+ return
+ else:
+ return
+
+ for child in node._get_childNodes():
+ if child._get_localName() == ELEMENT_USER:
+ if child.firstChild is not None:
+ user = child.firstChild.data.encode("utf-8")
+ elif child._get_localName() == ELEMENT_PASSWORD:
+ if child.firstChild is not None:
+ password = child.firstChild.data.encode("utf-8")
+
+ self.authentication = ("basic", user, password,)
+
+ def _parseDetails(self):
+ # Extract scheme, host, port and path
+ if self.uri.startswith("http://"):
+ self.ssl = False
+ rest = self.uri[7:]
+ elif self.uri.startswith("https://"):
+ self.ssl = True
+ rest = self.uri[8:]
+
+ splits = rest.split("/", 1)
+ hostport = splits[0].split(":")
+ self.host = hostport[0]
+ if len(hostport) > 1:
+ self.port = int(hostport[1])
+ else:
+ self.port = {False:80, True:443}[self.ssl]
+ self.path = "/"
+ if len(splits) > 1:
+ self.path += splits[1]
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,990 +0,0 @@
-##
-# Copyright (c) 2006-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-iTIP (RFC2446) processing.
-"""
-
-#
-# This is currently used for handling auto-replies to schedule requests arriving
-# in an inbox. It is called in a delayed fashion via reactor.callLater.
-#
-# We assume that all the components/calendars we deal with have been determined
-# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
-# types, etc.
-#
-# The logic for component matching needs a lot more work as it currently does not
-# know how to deal with overridden instances.
-#
-
-import datetime
-import md5
-import time
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-from twisted.web2.dav import davxml
-from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twisted.web2.dav.util import joinURL
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.resource import AccessDeniedError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
-from twistedcaldav.resource import isCalendarCollectionResource
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
- "handleRequest",
- "canAutoRespond",
-]
-
-class iTipException(Exception):
- pass
-
-def handleRequest(request, principal, inbox, calendar, child):
- """
- Handle an iTIP response automatically.
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- @return: L{Deferred} that is a L{deferredGenerator}
- """
-
- method = calendar.propertyValue("METHOD")
- if method == "REQUEST":
- f = processRequest
- elif method == "ADD":
- f = processAdd
- elif method == "CANCEL":
- f = processCancel
-
- return f(request, principal, inbox, calendar, child)
-
- at inlineCallbacks
-def processRequest(request, principal, inbox, calendar, child):
- """
- Process a METHOD=REQUEST.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 1. If so,
- 1. Remove existing ones in Inbox.
- 2. See if this updates existing ones in free-busy-set calendars.
- 3. Remove existing ones in those calendars.
- 4. See if this fits into a free slot:
- 1. If not, send REPLY with failure status
- 2. If so
- 1. send REPLY with success
- 2. add to f-b-s calendar
- 2. If not,
- 1. remove the one we got - its 'stale'
- 3. Delete the request from the Inbox.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
-
- log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
- processed = "ignored"
-
- # First determine whether this is a full or partial update. A full update is one containing the master
- # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
- # being changed.
-
- new_master = calendar.masterComponent()
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- if new_master:
- # So we have a full update. That means we need to delete any existing events completely and
- # replace with the ones provided so long as the new one is newer.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # See whether the new component is older than any existing ones and throw it away if so
- newinfo = (None,) + getComponentSyncInfo(new_master)
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- if old_master:
- info = getSyncInfo(calmatch, cal)
- else:
- info = None
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- else:
- processed = "older"
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if accepted:
- if calmatch:
- newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
- log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
- else:
- if calmatch:
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
-
- # Send a reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- if newchild:
- newInboxResource(child, newchild)
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- else:
- # So we have a partial update. That means we have to do partial updates to instances in
- # the existing calendar component.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # Check each component to see whether its new
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- processed = "older"
- new_components = [component for component in calendar.subcomponents()]
- for component in new_components:
- if component.name() == "VTIMEZONE":
- continue
-
- newinfo = (None,) + getComponentSyncInfo(component)
- old_component = findMatchingComponent(component, cal)
- if old_component:
- info = (None,) + getComponentSyncInfo(old_component)
- elif old_master:
- info = (None,) + getComponentSyncInfo(old_master)
- else:
- info = None
-
- if info is None or compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- processed = "processed"
- else:
- calendar.removeComponent(component)
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
-
- try:
- if calmatch:
- # Merge the new instances with the old ones
- mergeComponents(calendar, cal)
- newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
- log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
- else:
- if accepted:
- newchild = yield writeResource(request, calURL, updatecal, None, calendar)
- log.info("Added new calendar component in %s." % (calURL,))
-
- # Do reply if needed.
- if doreply:
- log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
- newchild = yield writeReply(request, principal, replycal, inbox)
- if newchild:
- newInboxResource(child, newchild)
-
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
-def processAdd(request, principal, inbox, calendar, child):
- """
- Process a METHOD=ADD.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
- log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
- raise NotImplementedError()
-
- at inlineCallbacks
-def processCancel(request, principal, inbox, calendar, child):
- """
- Process a METHOD=CANCEL.
-
- Policy find all components that match UID, SEQ and R-ID and remove them.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 2. Remove existing ones in Inbox.
- 3. See if this updates existing ones in free-busy-set calendars.
- 4. Remove existing ones in those calendars.
- 5. Remove the incoming request.
-
- NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
- instance may be cancelled. What we need to do for this is:
-
- 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
- delete the old one.
- 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
- leave the new and existing ones in the Inbox.
- 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
- remove the overridden component from the f-b-s resource.
- 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
-
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
- """
-
- log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
- processed = "ignored"
-
- # Get all component info for this iTIP message
- newinfo = getSyncInfo(child.fp.basename(), calendar)
- info = getAllInfo(inbox, calendar, child)
-
- # First see if we have a recurrence id which will force extra work
- has_rid = False
- if newinfo[4] is not None:
- has_rid = True
- else:
- for i in info:
- if i[4] is not None:
- has_rid = True
- break
-
- if not has_rid:
- # Compare the new one with each existing one.
- delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
- if delete_child:
- return
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # See whether the current component is older than any existing ones and throw it away if so
- cal = updatecal.iCalendar(calmatch)
- info = getSyncInfo(calmatch, cal)
- if compareSyncInfo(info, newinfo) < 0:
- # Delete existing resource which has been cancelled
- try:
- yield deleteResource(updatecal, calmatch)
- log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
- else:
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
-
- # If we have a match then we need to check whether we are updating etc
- if calmatch:
- # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
- # So we need to iterate over each iTIP component.
-
- # Get the existing calendar object
- existing_calendar = updatecal.iCalendar(calmatch)
- existing_master = existing_calendar.masterComponent()
- exdates = []
-
- for component in calendar.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- # Find matching component in existing calendar
- old_component = findMatchingComponent(component, existing_calendar)
-
- if old_component:
- # We are cancelling an overridden component, so we need to check the
- # SEQUENCE/DTSAMP with the master.
- if compareComponents(old_component, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # Remove the existing component.
- existing_calendar.removeComponent(old_component)
- elif existing_master:
- # We are trying to CANCEL a non-overridden instance, so we need to
- # check SEQUENCE/DTSTAMP with the master.
- if compareComponents(existing_master, component) < 0:
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # If we have any EXDATEs lets add them to the existing calendar object and write
- # it back.
- if exdates:
- if existing_master:
- existing_master.addProperty(Property("EXDATE", exdates))
-
- # See if there are still components in the calendar - we might have deleted the last overridden instance
- # in which case the calendar object is empty (except for VTIMEZONEs).
- if existing_calendar.mainType() is None:
- # Delete the now empty calendar object
- yield deleteResource(updatecal, calmatch)
- log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
- else:
- # Update the existing calendar object
- yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
- log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
-
- # Remove the now processed incoming request.
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
- child.fp.basename(),
- {
- "processed": "processed",
- "older" : "ignored: older",
- "ignored" : "ignored: no match"
- }[processed]
- ))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- at inlineCallbacks
-def checkForReply(request, principal, calendar):
- """
- Check whether a reply to the given iTIP message is needed. We will not process a reply
- if RSVP=FALSE. A reply will either be positive (accepted
- invitation) or negative (denied invitation). In addition we will modify calendar to reflect
- any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
-
- BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
- At the moment we will treat a failure on one instances as a DECLINE of the entire set.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param calendar: the L{Component} for the iTIP message we are processing.
- @return: C{True} if a reply is needed, C{False} otherwise.
- """
-
- # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
- accepted = True
-
- # First expand current one to get instances (only go 1 year into the future)
- default_future_expansion_duration = datetime.timedelta(days=356*1)
- expand_max = datetime.date.today() + default_future_expansion_duration
- instances = calendar.expandTimeRanges(expand_max)
-
- # Extract UID from primary component as we want to ignore this one if we match it
- # in any calendars.
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now compare each instance time-range with the index and see if there is an overlap
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- for calURL in fbset:
- testcal = yield request.locateResource(calURL)
-
- # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
- fbinfo = ([], [], [])
-
- # Now do search for overlapping time-range
- for instance in instances.instances.itervalues():
- try:
- tr = caldavxml.TimeRange(start="20000101", end="20000101")
- tr.start = instance.start
- tr.end = instance.end
- yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
-
- # If any fbinfo entries exist we have an overlap
- if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
- accepted = False
- break
- except NumberOfMatchesWithinLimits:
- accepted = False
- log.info("Exceeded number of matches whilst trying to find free-time.")
- break
-
- if not accepted:
- break
-
- # Extract the ATTENDEE property matching current recipient from the calendar data
- cuas = principal.calendarUserAddresses()
- attendeeProps = calendar.getAttendeeProperties(cuas)
- if not attendeeProps:
- returnValue((False, None, accepted))
-
- # Look for specific parameters
- rsvp = True
- for attendeeProp in attendeeProps:
- if "RSVP" in attendeeProp.params():
- if attendeeProp.params()["RSVP"][0] == "FALSE":
- rsvp = False
-
- # Now modify the original component
- del attendeeProp.params()["RSVP"]
-
- if accepted:
- partstat = "ACCEPTED"
- else:
- partstat = "DECLINED"
- for attendeeProp in attendeeProps:
- if "PARTSTAT" in attendeeProp.params():
- attendeeProp.params()["PARTSTAT"][0] = partstat
- else:
- attendeeProp.params()["PARTSTAT"] = [partstat]
-
- # Now create a new calendar object for the reply
-
- # First get useful props from the original
- replycal = calendar.duplicate()
-
- # Change METHOD
- replycal.getProperty("METHOD").setValue("REPLY")
-
- # Change PRODID to this server
- replycal.getProperty("PRODID").setValue(iCalendarProductID)
-
- # Add REQUEST-STATUS
- for component in replycal.subcomponents():
- if accepted:
- component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
- else:
- component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
- # Remove all attendees other than ourselves
- for component in replycal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
- attendeeProp = component.getAttendeeProperty(cuas)
- attendees = tuple(component.properties("ATTENDEE"))
- for attendee in attendees:
- if attendeeProp is None or (attendee.value() != attendeeProp.value()):
- component.removeProperty(attendee)
-
- returnValue((rsvp, replycal, accepted))
-
- at inlineCallbacks
-def writeReply(request, principal, replycal, ainbox):
- """
- Write an iTIP message reply into the specified Inbox.
-
- @param request: the L{twisted.web2.server.Request} for the current request.
- @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
- @param replycal: the L{Component} for the iTIP message reply.
- @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
- """
-
- # Get the Inbox of the ORGANIZER
- organizer = replycal.getOrganizer()
- assert organizer is not None
- organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
- assert organizerPrincipal is not None
- inboxURL = organizerPrincipal.scheduleInboxURL()
- assert inboxURL
-
- # Check for local or partitioned organizer
- if organizerPrincipal.locallyHosted():
- # Determine whether current principal has CALDAV:schedule right on that Inbox
- inbox = yield request.locateResource(inboxURL)
-
- try:
- yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
- except AccessDeniedError:
- log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
- returnValue(None)
-
- # Now deposit the new calendar into the inbox
- result = yield writeResource(request, inboxURL, inbox, None, replycal)
- else:
- # Send reply to Organizer
-
- # This is a local CALDAV scheduling operation.
- from twistedcaldav.scheduling.scheduler import CalDAVScheduler
- scheduler = CalDAVScheduler(request, ainbox)
-
- # Do the POST processing treating
- yield scheduler.doSchedulingViaPUT(tuple(principal.calendarUserAddresses())[0], (organizer,), replycal, True)
- result = None
-
- if accountingEnabled("iTIP", organizerPrincipal):
- emitAccounting(
- "iTIP", organizerPrincipal,
- "Originator: %s\nRecipients: %s\n\n%s"
- % (principal.principalURL(), str(organizer), str(replycal))
- )
-
- returnValue(result)
-
- at inlineCallbacks
-def writeResource(request, collURL, collection, name, calendar):
- """
- Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
- resource or by creating a new one.
-
- @param request: the L{IRequest} for the current request.
- @param collURL: the C{str} containing the URL of the calendar collection.
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @param calendar: the L{Component} calendar to write.
- @return: C{tuple} of L{Deferred}, L{CalDAVFile}
- """
-
- # Create a new name if one was not provided
- if name is None:
- name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- newchildURL = joinURL(collURL, name)
- newchild = yield request.locateResource(newchildURL)
-
- # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
- # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
- # NB Only do this when writing to something other than an Inbox or Outbox
- itipper = True
- if collection.isCalendarCollection():
- method = calendar.getProperty("METHOD")
- if method:
- calendar.removeProperty(method)
- itipper = False
-
- # Now write it to the resource
- try:
- yield storeCalendarObjectResource(
- request=request,
- sourcecal = False,
- destination = newchild,
- destination_uri = newchildURL,
- calendardata = str(calendar),
- destinationparent = collection,
- destinationcal = True,
- isiTIP = itipper
- )
- except:
- # FIXME: bare except
- return
-
- returnValue(newchild)
-
-def newInboxResource(child, newchild):
- """
- Copy recipient and organizer properties from one iTIP resource, to another,
- switching them as appropriate for a reply, and also set the state.
-
- @param child: the L{CalDAVFile} for the original iTIP message.
- @param newchild: the L{CalDAVFile} for the iTIP message reply.
- """
- # Make previous Recipient the new Originator
- if child.hasDeadProperty(caldavxml.Recipient):
- recip = child.readDeadProperty(caldavxml.Recipient)
- if recip.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-
- # Make previous Originator the new Recipient
- if child.hasDeadProperty(caldavxml.Originator):
- orig = child.readDeadProperty(caldavxml.Originator)
- if orig.children:
- # Store CALDAV:originator property
- newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-
-def deleteResource(collection, name):
- """
- Delete the calendar resource in the specified calendar.
-
- @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
- @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
- @return: L{Deferred}
- """
-
- delchild = collection.getChild(name)
- index = collection.index()
- index.deleteResource(delchild.fp.basename())
-
- def _deletedResourced(result):
- # Change CTag on the parent calendar collection
- return collection.updateCTag().addCallback(lambda _: result)
-
- d = maybeDeferred(delete, "", delchild.fp, "0")
- d.addCallback(_deletedResourced)
- return d
-
-def canAutoRespond(calendar):
- """
- Check whether the METHOD of this iTIP calendar object is one we can process. Also,
- we will only handle VEVENTs right now.
-
- @param calendar: L{Component} for calendar to examine.
- @return: C{True} if we can auto-respond, C{False} if not.
- """
-
- try:
- method = calendar.propertyValue("METHOD")
- if method not in ("REQUEST", "ADD", "CANCEL"):
- return False
- if calendar.mainType() not in ("VEVENT"):
- return False
- except ValueError:
- return False
-
- return True
-
- at inlineCallbacks
-def processOthersInInbox(info, newinfo, inbox, child):
- # Compare the new one with each existing one.
- delete_child = False
- for i in info:
- # For any that are older, delete them.
- if compareSyncInfo(i, newinfo) < 0:
- try:
- yield deleteResource(inbox, i[0])
- log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- else:
- # For any that are newer or the same, mark the new one to be deleted.
- delete_child = True
-
- # Delete the new one if so marked.
- if delete_child:
- try:
- yield deleteResource(inbox, child.fp.basename())
- log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- returnValue(delete_child)
-
- at inlineCallbacks
-def findCalendarMatch(request, principal, calendar):
- # Try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
-
- # Find the current recipients calendar-free-busy-set
- fbset = yield principal.calendarFreeBusyURIs(request)
-
- # Find the first calendar in the list with a component matching the one we are processing
- calmatch = None
- updatecal = None
- calURL = None
- for calURL in fbset:
- updatecal = yield request.locateResource(calURL)
- if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
- # We will ignore missing calendars. If the recipient has failed to
- # properly manage the free busy set that should not prevent us from working.
- continue
- calmatch = matchComponentInCalendar(updatecal, calendar)
- if calmatch:
- log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
- break
-
- if calmatch is None and len(fbset):
- calURL = fbset[0]
- updatecal = yield request.locateResource(calURL)
-
- returnValue((calmatch, updatecal, calURL))
-
-def matchComponentInCalendar(collection, calendar):
- """
- See if the component in the provided iTIP calendar object matches any in the specified calendar
- collection.
-
- @param collection: L{CalDAVFile} for the calendar collection to examine.
- @param calendar: L{Component} for calendar to examine.
- @return: C{list} of resource names found.
- """
-
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- result = index.resourceNamesForUID(uid)
-
- # There can be only one
- if len(result) > 0:
- return result[0]
- else:
- return None
- except ValueError:
- return None
-
-def findMatchingComponent(component, calendar):
- """
- See if any overridden component in the provided iTIP calendar object matches the specified component.
-
- @param component: the component to try and match.
- @type component: L{Component}
- @param calendar: the calendar to find a match in.
- @type calendar: L{Component}
- @return: L{Component} for matching component,
- or C{None} if not found.
- """
-
- # Extract RECURRENCE-ID value from component
- rid = component.getRecurrenceIDUTC()
-
- # Return the one that matches in the calendar
- return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
- """
- Merge the overridden instance components in newcal into oldcal replacing any
- matching components there.
-
- @param newcal: the new overridden instances to use.
- @type newcal: L{Component}
- @param oldcal: the component to merge into.
- @type oldcal: L{Component}
- """
-
- # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
- # use a TZID that is already specified in the old component set.
-
- # We will update the SEQUENCE on the master to the highest value of the current one on the master
- # or the ones in the components we are changing.
-
- for component in newcal.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- rid = component.getRecurrenceIDUTC()
- old_component = oldcal.overriddenComponent(rid)
- if old_component:
- oldcal.removeComponent(old_component)
- oldcal.addComponent(component)
-
-def getAllInfo(collection, calendar, ignore):
- """
- Find each component in the calendar collection that has a matching UID with
- the supplied component, and get useful synchronization details from it, ignoring
- the one with the supplied resource name.
-
- @param collection: the L{CalDAVFile} for the calendar collection.
- @param calendar: the L{Component} for the component being compared with.
- @param ignore: the C{str} containing the name of a resource to ignore,
- or C{None} if none to ignore.
- @return: C{list} of synchronization information for each resource found.
- """
- names = []
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- names = index.resourceNamesForUID(uid)
-
- # Remove the one we want to ignore
- if ignore is not None:
- names = [name for name in names if name != ignore.fp.basename()]
- except ValueError:
- return []
-
- # Now get info for each name
- result = []
- for name in names:
- cal = collection.iCalendar(name)
- result.append(getSyncInfo(name, cal))
-
- return result
-
-def getSyncInfo(name, calendar):
- """
- Get property value details needed to synchronize iTIP components.
-
- @param calendar: L{Component} for calendar to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract components from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
-
- except ValueError:
- return (name, None, None, None, None)
-
- return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
- """
- Get property value details needed to synchronize iTIP components.
-
- @param component: L{Component} to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract items from component
- uid = component.propertyValue("UID")
- seq = component.propertyValue("SEQUENCE")
- if seq:
- seq = int(seq)
- dtstamp = component.propertyValue("DTSTAMP")
- rid = component.propertyValue("RECURRENCE-ID")
-
- except ValueError:
- return (None, None, None, None)
-
- return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
- """
- Compare synchronization information for two components to see if they match according to iTIP.
-
- @param component1: first component to check.
- @type component1: L{Component}
- @param component2: second component to check.
- @type component2: L{Component}
-
- @return: 0, 1, -1 as per compareSyncInfo.
- """
- info1 = (None,) + getComponentSyncInfo(component1)
- info2 = (None,) + getComponentSyncInfo(component2)
- return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
- """
- Compare two synchronization information records.
-
- @param info1: a C{tuple} as returned by L{getSyncInfo}.
- @param info2: a C{tuple} as returned by L{getSyncInfo}.
- @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
- """
- # UIDs MUST match
- assert info1[1] == info2[1]
-
- # Look for sequence
- if (info1[2] is not None) and (info2[2] is not None):
- if info1[2] > info2[2]:
- return 1
- if info1[2] < info2[2]:
- return -1
- elif (info1[2] is not None) and (info2[2] is None):
- return 1
- elif (info1[2] is None) and (info2[2] is not None):
- return -1
-
- # Look for DTSTAMP
- if (info1[3] is not None) and (info2[3] is not None):
- if info1[3] > info2[3]:
- return 1
- if info1[3] < info2[3]:
- return -1
- elif (info1[3] is not None) and (info2[3] is None):
- return 1
- elif (info1[3] is None) and (info2[3] is not None):
- return -1
-
- return 0
-
-class iTIPRequestStatus(object):
- """
- String constants for various iTIP status codes we use.
- """
-
- MESSAGE_PENDING_CODE = "1.0"
- MESSAGE_SENT_CODE = "1.1"
- MESSAGE_DELIVERED_CODE = "1.2"
-
- MESSAGE_PENDING = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
- MESSAGE_SENT = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
- MESSAGE_DELIVERED = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
-
- SUCCESS = "2.0;Success"
-
- INVALID_CALENDAR_USER = "3.7;Invalid Calendar User"
- NO_AUTHORITY = "3.8;No authority"
-
- BAD_REQUEST = "5.0;Service cannot handle request"
- SERVICE_UNAVAILABLE = "5.1;Service unavailable"
- INVALID_SERVICE = "5.2;Invalid calendar service"
- NO_USER_SUPPORT = "5.3;No scheduling support for user"
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/itip.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/itip.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,990 @@
+##
+# Copyright (c) 2006-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+iTIP (RFC2446) processing.
+"""
+
+#
+# This is currently used for handling auto-replies to schedule requests arriving
+# in an inbox. It is called in a delayed fashion via reactor.callLater.
+#
+# We assume that all the components/calendars we deal with have been determined
+# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
+# types, etc.
+#
+# The logic for component matching needs a lot more work as it currently does not
+# know how to deal with overridden instances.
+#
+
+import datetime
+import md5
+import time
+
+from twisted.python.failure import Failure
+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
+from twisted.web2.dav import davxml
+from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
+from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.fileop import delete
+from twisted.web2.dav.resource import AccessDeniedError
+
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.log import Logger
+from twistedcaldav.ical import Property, iCalendarProductID
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+
+log = Logger()
+
+__version__ = "0.0"
+
+__all__ = [
+ "handleRequest",
+ "canAutoRespond",
+]
+
+class iTipException(Exception):
+ pass
+
+def handleRequest(request, principal, inbox, calendar, child):
+ """
+ Handle an iTIP response automatically.
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+ @param calendar: the L{Component} for the iTIP message we are processing.
+ @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+ @return: L{Deferred} that is a L{deferredGenerator}
+ """
+
+ method = calendar.propertyValue("METHOD")
+ if method == "REQUEST":
+ f = processRequest
+ elif method == "ADD":
+ f = processAdd
+ elif method == "CANCEL":
+ f = processCancel
+
+ return f(request, principal, inbox, calendar, child)
+
+ at inlineCallbacks
+def processRequest(request, principal, inbox, calendar, child):
+ """
+ Process a METHOD=REQUEST.
+
+ Steps:
+
+ 1. See if this updates existing ones in Inbox.
+ 1. If so,
+ 1. Remove existing ones in Inbox.
+ 2. See if this updates existing ones in free-busy-set calendars.
+ 3. Remove existing ones in those calendars.
+ 4. See if this fits into a free slot:
+ 1. If not, send REPLY with failure status
+ 2. If so
+ 1. send REPLY with success
+ 2. add to f-b-s calendar
+ 2. If not,
+ 1. remove the one we got - its 'stale'
+ 3. Delete the request from the Inbox.
+
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+ @param calendar: the L{Component} for the iTIP message we are processing.
+ @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+ """
+
+ log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
+ processed = "ignored"
+
+ # First determine whether this is a full or partial update. A full update is one containing the master
+ # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
+ # being changed.
+
+ new_master = calendar.masterComponent()
+
+ # Next we want to try and find a match to any components on existing calendars listed as contributing
+ # to free-busy as we will need to update those with the new one.
+ calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+
+ if new_master:
+ # So we have a full update. That means we need to delete any existing events completely and
+ # replace with the ones provided so long as the new one is newer.
+
+ # If we have a match then we need to check whether we are updating etc
+ check_reply = False
+ if calmatch:
+ # See whether the new component is older than any existing ones and throw it away if so
+ newinfo = (None,) + getComponentSyncInfo(new_master)
+ cal = updatecal.iCalendar(calmatch)
+ old_master = cal.masterComponent()
+ if old_master:
+ info = getSyncInfo(calmatch, cal)
+ else:
+ info = None
+ if info is None or compareSyncInfo(info, newinfo) < 0:
+ # Existing resource is older and will be replaced
+ check_reply = True
+ else:
+ processed = "older"
+ else:
+ # We have a new request which we can reply to
+ check_reply = True
+
+ if check_reply:
+ # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+ doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
+
+ try:
+ if accepted:
+ if calmatch:
+ newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
+ log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+ else:
+ newchild = yield writeResource(request, calURL, updatecal, None, calendar)
+ log.info("Added new calendar component in %s." % (calURL,))
+ else:
+ if calmatch:
+ yield deleteResource(updatecal, calmatch)
+ log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+
+ # Send a reply if needed.
+ if doreply:
+ log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+ newchild = yield writeReply(request, principal, replycal, inbox)
+ if newchild:
+ newInboxResource(child, newchild)
+ processed = "processed"
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
+ else:
+ # So we have a partial update. That means we have to do partial updates to instances in
+ # the existing calendar component.
+
+ # If we have a match then we need to check whether we are updating etc
+ check_reply = False
+ if calmatch:
+ # Check each component to see whether its new
+ cal = updatecal.iCalendar(calmatch)
+ old_master = cal.masterComponent()
+ processed = "older"
+ new_components = [component for component in calendar.subcomponents()]
+ for component in new_components:
+ if component.name() == "VTIMEZONE":
+ continue
+
+ newinfo = (None,) + getComponentSyncInfo(component)
+ old_component = findMatchingComponent(component, cal)
+ if old_component:
+ info = (None,) + getComponentSyncInfo(old_component)
+ elif old_master:
+ info = (None,) + getComponentSyncInfo(old_master)
+ else:
+ info = None
+
+ if info is None or compareSyncInfo(info, newinfo) < 0:
+ # Existing resource is older and will be replaced
+ check_reply = True
+ processed = "processed"
+ else:
+ calendar.removeComponent(component)
+ else:
+ # We have a new request which we can reply to
+ check_reply = True
+
+ if check_reply:
+ # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+ doreply, replycal, accepted = yield checkForReply(request, principal, calendar)
+
+ try:
+ if calmatch:
+ # Merge the new instances with the old ones
+ mergeComponents(calendar, cal)
+ newchild = yield writeResource(request, calURL, updatecal, calmatch, cal)
+ log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+ else:
+ if accepted:
+ newchild = yield writeResource(request, calURL, updatecal, None, calendar)
+ log.info("Added new calendar component in %s." % (calURL,))
+
+ # Do reply if needed.
+ if doreply:
+ log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+ newchild = yield writeReply(request, principal, replycal, inbox)
+ if newchild:
+ newInboxResource(child, newchild)
+
+ processed = "processed"
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
+ # Remove the now processed incoming request.
+ try:
+ yield deleteResource(inbox, child.fp.basename())
+ log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
+ child.fp.basename(),
+ {
+ "processed": "processed",
+ "older" : "ignored: older",
+ "ignored" : "ignored: no match"
+ }[processed]
+ ))
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
+def processAdd(request, principal, inbox, calendar, child):
+ """
+ Process a METHOD=ADD.
+
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+ @param calendar: the L{Component} for the iTIP message we are processing.
+ @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+ """
+ log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
+ raise NotImplementedError()
+
+ at inlineCallbacks
+def processCancel(request, principal, inbox, calendar, child):
+ """
+ Process a METHOD=CANCEL.
+
+ Policy find all components that match UID, SEQ and R-ID and remove them.
+
+ Steps:
+
+ 1. See if this updates existing ones in Inbox.
+ 2. Remove existing ones in Inbox.
+ 3. See if this updates existing ones in free-busy-set calendars.
+ 4. Remove existing ones in those calendars.
+ 5. Remove the incoming request.
+
+ NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
+ instance may be cancelled. What we need to do for this is:
+
+ 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
+ delete the old one.
+ 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
+ leave the new and existing ones in the Inbox.
+ 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
+ remove the overridden component from the f-b-s resource.
+ 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
+
+ TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+ @param calendar: the L{Component} for the iTIP message we are processing.
+ @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+ """
+
+ log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
+ processed = "ignored"
+
+ # Get all component info for this iTIP message
+ newinfo = getSyncInfo(child.fp.basename(), calendar)
+ info = getAllInfo(inbox, calendar, child)
+
+ # First see if we have a recurrence id which will force extra work
+ has_rid = False
+ if newinfo[4] is not None:
+ has_rid = True
+ else:
+ for i in info:
+ if i[4] is not None:
+ has_rid = True
+ break
+
+ if not has_rid:
+ # Compare the new one with each existing one.
+ delete_child = yield processOthersInInbox(info, newinfo, inbox, child)
+ if delete_child:
+ return
+
+ # Next we want to try and find a match to any components on existing calendars listed as contributing
+ # to free-busy as we will need to update those with the new one.
+ calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+
+ # If we have a match then we need to check whether we are updating etc
+ if calmatch:
+ # See whether the current component is older than any existing ones and throw it away if so
+ cal = updatecal.iCalendar(calmatch)
+ info = getSyncInfo(calmatch, cal)
+ if compareSyncInfo(info, newinfo) < 0:
+ # Delete existing resource which has been cancelled
+ try:
+ yield deleteResource(updatecal, calmatch)
+ log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+ processed = "processed"
+ else:
+ processed = "older"
+ else:
+ # Nothing to do except delete the inbox item as we have nothing to cancel.
+ processed = "ignored"
+ else:
+ # Try and find a match to any components on existing calendars listed as contributing
+ # to free-busy as we will need to update those with the new one.
+ calmatch, updatecal, calURL = yield findCalendarMatch(request, principal, calendar)
+
+ # If we have a match then we need to check whether we are updating etc
+ if calmatch:
+ # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
+ # So we need to iterate over each iTIP component.
+
+ # Get the existing calendar object
+ existing_calendar = updatecal.iCalendar(calmatch)
+ existing_master = existing_calendar.masterComponent()
+ exdates = []
+
+ for component in calendar.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+
+ # Find matching component in existing calendar
+ old_component = findMatchingComponent(component, existing_calendar)
+
+ if old_component:
+ # We are cancelling an overridden component, so we need to check the
+ # SEQUENCE/DTSAMP with the master.
+ if compareComponents(old_component, component) < 0:
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+
+ # Remove the existing component.
+ existing_calendar.removeComponent(old_component)
+ elif existing_master:
+ # We are trying to CANCEL a non-overridden instance, so we need to
+ # check SEQUENCE/DTSTAMP with the master.
+ if compareComponents(existing_master, component) < 0:
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+
+ # If we have any EXDATEs lets add them to the existing calendar object and write
+ # it back.
+ if exdates:
+ if existing_master:
+ existing_master.addProperty(Property("EXDATE", exdates))
+
+ # See if there are still components in the calendar - we might have deleted the last overridden instance
+ # in which case the calendar object is empty (except for VTIMEZONEs).
+ if existing_calendar.mainType() is None:
+ # Delete the now empty calendar object
+ yield deleteResource(updatecal, calmatch)
+ log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+ else:
+ # Update the existing calendar object
+ yield writeResource(request, calURL, updatecal, calmatch, existing_calendar)
+ log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
+ processed = "processed"
+ else:
+ processed = "older"
+ else:
+ # Nothing to do except delete the inbox item as we have nothing to cancel.
+ processed = "ignored"
+
+ # Remove the now processed incoming request.
+ try:
+ yield deleteResource(inbox, child.fp.basename())
+ log.info("Deleted new iTIP message %s in Inbox because it has been %s." % (
+ child.fp.basename(),
+ {
+ "processed": "processed",
+ "older" : "ignored: older",
+ "ignored" : "ignored: no match"
+ }[processed]
+ ))
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
+ at inlineCallbacks
+def checkForReply(request, principal, calendar):
+ """
+ Check whether a reply to the given iTIP message is needed. We will not process a reply
+ if RSVP=FALSE. A reply will either be positive (accepted
+ invitation) or negative (denied invitation). In addition we will modify calendar to reflect
+ any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
+
+ BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
+ At the moment we will treat a failure on one instances as a DECLINE of the entire set.
+
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param calendar: the L{Component} for the iTIP message we are processing.
+ @return: C{True} if a reply is needed, C{False} otherwise.
+ """
+
+ # We need to figure out whether the specified component will clash with any others in the f-b-set calendars
+ accepted = True
+
+ # First expand current one to get instances (only go 1 year into the future)
+ default_future_expansion_duration = datetime.timedelta(days=356*1)
+ expand_max = datetime.date.today() + default_future_expansion_duration
+ instances = calendar.expandTimeRanges(expand_max)
+
+ # Extract UID from primary component as we want to ignore this one if we match it
+ # in any calendars.
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now compare each instance time-range with the index and see if there is an overlap
+ fbset = yield principal.calendarFreeBusyURIs(request)
+
+ for calURL in fbset:
+ testcal = yield request.locateResource(calURL)
+
+ # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+ fbinfo = ([], [], [])
+
+ # Now do search for overlapping time-range
+ for instance in instances.instances.itervalues():
+ try:
+ tr = caldavxml.TimeRange(start="20000101", end="20000101")
+ tr.start = instance.start
+ tr.end = instance.end
+ yield report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid)
+
+ # If any fbinfo entries exist we have an overlap
+ if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+ accepted = False
+ break
+ except NumberOfMatchesWithinLimits:
+ accepted = False
+ log.info("Exceeded number of matches whilst trying to find free-time.")
+ break
+
+ if not accepted:
+ break
+
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = principal.calendarUserAddresses()
+ attendeeProps = calendar.getAttendeeProperties(cuas)
+ if not attendeeProps:
+ returnValue((False, None, accepted))
+
+ # Look for specific parameters
+ rsvp = True
+ for attendeeProp in attendeeProps:
+ if "RSVP" in attendeeProp.params():
+ if attendeeProp.params()["RSVP"][0] == "FALSE":
+ rsvp = False
+
+ # Now modify the original component
+ del attendeeProp.params()["RSVP"]
+
+ if accepted:
+ partstat = "ACCEPTED"
+ else:
+ partstat = "DECLINED"
+ for attendeeProp in attendeeProps:
+ if "PARTSTAT" in attendeeProp.params():
+ attendeeProp.params()["PARTSTAT"][0] = partstat
+ else:
+ attendeeProp.params()["PARTSTAT"] = [partstat]
+
+ # Now create a new calendar object for the reply
+
+ # First get useful props from the original
+ replycal = calendar.duplicate()
+
+ # Change METHOD
+ replycal.getProperty("METHOD").setValue("REPLY")
+
+ # Change PRODID to this server
+ replycal.getProperty("PRODID").setValue(iCalendarProductID)
+
+ # Add REQUEST-STATUS
+ for component in replycal.subcomponents():
+ if accepted:
+ component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+ else:
+ component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
+
+ # Remove all attendees other than ourselves
+ for component in replycal.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ attendeeProp = component.getAttendeeProperty(cuas)
+ attendees = tuple(component.properties("ATTENDEE"))
+ for attendee in attendees:
+ if attendeeProp is None or (attendee.value() != attendeeProp.value()):
+ component.removeProperty(attendee)
+
+ returnValue((rsvp, replycal, accepted))
+
+ at inlineCallbacks
+def writeReply(request, principal, replycal, ainbox):
+ """
+ Write an iTIP message reply into the specified Inbox.
+
+ @param request: the L{twisted.web2.server.Request} for the current request.
+ @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+ @param replycal: the L{Component} for the iTIP message reply.
+ @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
+ """
+
+ # Get the Inbox of the ORGANIZER
+ organizer = replycal.getOrganizer()
+ assert organizer is not None
+ organizerPrincipal = ainbox.principalForCalendarUserAddress(organizer)
+ assert organizerPrincipal is not None
+ inboxURL = organizerPrincipal.scheduleInboxURL()
+ assert inboxURL
+
+ # Check for local or partitioned organizer
+ if organizerPrincipal.locallyHosted():
+ # Determine whether current principal has CALDAV:schedule right on that Inbox
+ inbox = yield request.locateResource(inboxURL)
+
+ try:
+ yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL())))
+ except AccessDeniedError:
+ log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
+ returnValue(None)
+
+ # Now deposit the new calendar into the inbox
+ result = yield writeResource(request, inboxURL, inbox, None, replycal)
+ else:
+ # Send reply to Organizer
+
+ # This is a local CALDAV scheduling operation.
+ from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+ scheduler = CalDAVScheduler(request, ainbox)
+
+ # Do the POST processing treating
+ yield scheduler.doSchedulingViaPUT(tuple(principal.calendarUserAddresses())[0], (organizer,), replycal, True)
+ result = None
+
+ if accountingEnabled("iTIP", organizerPrincipal):
+ emitAccounting(
+ "iTIP", organizerPrincipal,
+ "Originator: %s\nRecipients: %s\n\n%s"
+ % (principal.principalURL(), str(organizer), str(replycal))
+ )
+
+ returnValue(result)
+
+ at inlineCallbacks
+def writeResource(request, collURL, collection, name, calendar):
+ """
+ Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
+ resource or by creating a new one.
+
+ @param request: the L{IRequest} for the current request.
+ @param collURL: the C{str} containing the URL of the calendar collection.
+ @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+ @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+ @param calendar: the L{Component} calendar to write.
+ @return: C{tuple} of L{Deferred}, L{CalDAVFile}
+ """
+
+ # Create a new name if one was not provided
+ if name is None:
+ name = md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
+
+ # Get a resource for the new item
+ newchildURL = joinURL(collURL, name)
+ newchild = yield request.locateResource(newchildURL)
+
+ # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
+ # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
+ # NB Only do this when writing to something other than an Inbox or Outbox
+ itipper = True
+ if collection.isCalendarCollection():
+ method = calendar.getProperty("METHOD")
+ if method:
+ calendar.removeProperty(method)
+ itipper = False
+
+ # Now write it to the resource
+ try:
+ yield storeCalendarObjectResource(
+ request=request,
+ sourcecal = False,
+ destination = newchild,
+ destination_uri = newchildURL,
+ calendardata = str(calendar),
+ destinationparent = collection,
+ destinationcal = True,
+ isiTIP = itipper
+ )
+ except:
+ # FIXME: bare except
+ return
+
+ returnValue(newchild)
+
+def newInboxResource(child, newchild):
+ """
+ Copy recipient and organizer properties from one iTIP resource, to another,
+ switching them as appropriate for a reply, and also set the state.
+
+ @param child: the L{CalDAVFile} for the original iTIP message.
+ @param newchild: the L{CalDAVFile} for the iTIP message reply.
+ """
+ # Make previous Recipient the new Originator
+ if child.hasDeadProperty(caldavxml.Recipient):
+ recip = child.readDeadProperty(caldavxml.Recipient)
+ if recip.children:
+ # Store CALDAV:originator property
+ newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
+
+ # Make previous Originator the new Recipient
+ if child.hasDeadProperty(caldavxml.Originator):
+ orig = child.readDeadProperty(caldavxml.Originator)
+ if orig.children:
+ # Store CALDAV:originator property
+ newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
+
+def deleteResource(collection, name):
+ """
+ Delete the calendar resource in the specified calendar.
+
+ @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+ @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+ @return: L{Deferred}
+ """
+
+ delchild = collection.getChild(name)
+ index = collection.index()
+ index.deleteResource(delchild.fp.basename())
+
+ def _deletedResourced(result):
+ # Change CTag on the parent calendar collection
+ return collection.updateCTag().addCallback(lambda _: result)
+
+ d = maybeDeferred(delete, "", delchild.fp, "0")
+ d.addCallback(_deletedResourced)
+ return d
+
+def canAutoRespond(calendar):
+ """
+ Check whether the METHOD of this iTIP calendar object is one we can process. Also,
+ we will only handle VEVENTs right now.
+
+ @param calendar: L{Component} for calendar to examine.
+ @return: C{True} if we can auto-respond, C{False} if not.
+ """
+
+ try:
+ method = calendar.propertyValue("METHOD")
+ if method not in ("REQUEST", "ADD", "CANCEL"):
+ return False
+ if calendar.mainType() not in ("VEVENT"):
+ return False
+ except ValueError:
+ return False
+
+ return True
+
+ at inlineCallbacks
+def processOthersInInbox(info, newinfo, inbox, child):
+ # Compare the new one with each existing one.
+ delete_child = False
+ for i in info:
+ # For any that are older, delete them.
+ if compareSyncInfo(i, newinfo) < 0:
+ try:
+ yield deleteResource(inbox, i[0])
+ log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+ else:
+ # For any that are newer or the same, mark the new one to be deleted.
+ delete_child = True
+
+ # Delete the new one if so marked.
+ if delete_child:
+ try:
+ yield deleteResource(inbox, child.fp.basename())
+ log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
+ except:
+ # FIXME: bare except
+ log.err("Error while auto-processing iTIP: %s" % (Failure(),))
+ raise iTipException()
+
+ returnValue(delete_child)
+
+ at inlineCallbacks
+def findCalendarMatch(request, principal, calendar):
+ # Try and find a match to any components on existing calendars listed as contributing
+ # to free-busy as we will need to update those with the new one.
+
+ # Find the current recipients calendar-free-busy-set
+ fbset = yield principal.calendarFreeBusyURIs(request)
+
+ # Find the first calendar in the list with a component matching the one we are processing
+ calmatch = None
+ updatecal = None
+ calURL = None
+ for calURL in fbset:
+ updatecal = yield request.locateResource(calURL)
+ if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
+ # We will ignore missing calendars. If the recipient has failed to
+ # properly manage the free busy set that should not prevent us from working.
+ continue
+ calmatch = matchComponentInCalendar(updatecal, calendar)
+ if calmatch:
+ log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
+ break
+
+ if calmatch is None and len(fbset):
+ calURL = fbset[0]
+ updatecal = yield request.locateResource(calURL)
+
+ returnValue((calmatch, updatecal, calURL))
+
+def matchComponentInCalendar(collection, calendar):
+ """
+ See if the component in the provided iTIP calendar object matches any in the specified calendar
+ collection.
+
+ @param collection: L{CalDAVFile} for the calendar collection to examine.
+ @param calendar: L{Component} for calendar to examine.
+ @return: C{list} of resource names found.
+ """
+
+ try:
+ # Extract UID from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now use calendar collection index to find all other resources with the same UID
+ index = collection.index()
+ result = index.resourceNamesForUID(uid)
+
+ # There can be only one
+ if len(result) > 0:
+ return result[0]
+ else:
+ return None
+ except ValueError:
+ return None
+
+def findMatchingComponent(component, calendar):
+ """
+ See if any overridden component in the provided iTIP calendar object matches the specified component.
+
+ @param component: the component to try and match.
+ @type component: L{Component}
+ @param calendar: the calendar to find a match in.
+ @type calendar: L{Component}
+ @return: L{Component} for matching component,
+ or C{None} if not found.
+ """
+
+ # Extract RECURRENCE-ID value from component
+ rid = component.getRecurrenceIDUTC()
+
+ # Return the one that matches in the calendar
+ return calendar.overriddenComponent(rid)
+
+def mergeComponents(newcal, oldcal):
+ """
+ Merge the overridden instance components in newcal into oldcal replacing any
+ matching components there.
+
+ @param newcal: the new overridden instances to use.
+ @type newcal: L{Component}
+ @param oldcal: the component to merge into.
+ @type oldcal: L{Component}
+ """
+
+ # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
+ # use a TZID that is already specified in the old component set.
+
+ # We will update the SEQUENCE on the master to the highest value of the current one on the master
+ # or the ones in the components we are changing.
+
+ for component in newcal.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+
+ rid = component.getRecurrenceIDUTC()
+ old_component = oldcal.overriddenComponent(rid)
+ if old_component:
+ oldcal.removeComponent(old_component)
+ oldcal.addComponent(component)
+
+def getAllInfo(collection, calendar, ignore):
+ """
+ Find each component in the calendar collection that has a matching UID with
+ the supplied component, and get useful synchronization details from it, ignoring
+ the one with the supplied resource name.
+
+ @param collection: the L{CalDAVFile} for the calendar collection.
+ @param calendar: the L{Component} for the component being compared with.
+ @param ignore: the C{str} containing the name of a resource to ignore,
+ or C{None} if none to ignore.
+ @return: C{list} of synchronization information for each resource found.
+ """
+ names = []
+ try:
+ # Extract UID from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now use calendar collection index to find all other resources with the same UID
+ index = collection.index()
+ names = index.resourceNamesForUID(uid)
+
+ # Remove the one we want to ignore
+ if ignore is not None:
+ names = [name for name in names if name != ignore.fp.basename()]
+ except ValueError:
+ return []
+
+ # Now get info for each name
+ result = []
+ for name in names:
+ cal = collection.iCalendar(name)
+ result.append(getSyncInfo(name, cal))
+
+ return result
+
+def getSyncInfo(name, calendar):
+ """
+ Get property value details needed to synchronize iTIP components.
+
+ @param calendar: L{Component} for calendar to check.
+ @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+ """
+ try:
+ # Extract components from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
+
+ except ValueError:
+ return (name, None, None, None, None)
+
+ return (name, uid, seq, dtstamp, rid)
+
+def getComponentSyncInfo(component):
+ """
+ Get property value details needed to synchronize iTIP components.
+
+ @param component: L{Component} to check.
+ @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+ """
+ try:
+ # Extract items from component
+ uid = component.propertyValue("UID")
+ seq = component.propertyValue("SEQUENCE")
+ if seq:
+ seq = int(seq)
+ dtstamp = component.propertyValue("DTSTAMP")
+ rid = component.propertyValue("RECURRENCE-ID")
+
+ except ValueError:
+ return (None, None, None, None)
+
+ return (uid, seq, dtstamp, rid)
+
+def compareComponents(component1, component2):
+ """
+ Compare synchronization information for two components to see if they match according to iTIP.
+
+ @param component1: first component to check.
+ @type component1: L{Component}
+ @param component2: second component to check.
+ @type component2: L{Component}
+
+ @return: 0, 1, -1 as per compareSyncInfo.
+ """
+ info1 = (None,) + getComponentSyncInfo(component1)
+ info2 = (None,) + getComponentSyncInfo(component2)
+ return compareSyncInfo(info1, info2)
+
+def compareSyncInfo(info1, info2):
+ """
+ Compare two synchronization information records.
+
+ @param info1: a C{tuple} as returned by L{getSyncInfo}.
+ @param info2: a C{tuple} as returned by L{getSyncInfo}.
+ @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
+ """
+ # UIDs MUST match
+ assert info1[1] == info2[1]
+
+ # Look for sequence
+ if (info1[2] is not None) and (info2[2] is not None):
+ if info1[2] > info2[2]:
+ return 1
+ if info1[2] < info2[2]:
+ return -1
+ elif (info1[2] is not None) and (info2[2] is None):
+ return 1
+ elif (info1[2] is None) and (info2[2] is not None):
+ return -1
+
+ # Look for DTSTAMP
+ if (info1[3] is not None) and (info2[3] is not None):
+ if info1[3] > info2[3]:
+ return 1
+ if info1[3] < info2[3]:
+ return -1
+ elif (info1[3] is not None) and (info2[3] is None):
+ return 1
+ elif (info1[3] is None) and (info2[3] is not None):
+ return -1
+
+ return 0
+
+class iTIPRequestStatus(object):
+ """
+ String constants for various iTIP status codes we use.
+ """
+
+ MESSAGE_PENDING_CODE = "1.0"
+ MESSAGE_SENT_CODE = "1.1"
+ MESSAGE_DELIVERED_CODE = "1.2"
+
+ MESSAGE_PENDING = MESSAGE_PENDING_CODE + ";Scheduling message send is pending"
+ MESSAGE_SENT = MESSAGE_SENT_CODE + ";Scheduling message has been sent"
+ MESSAGE_DELIVERED = MESSAGE_DELIVERED_CODE + ";Scheduling message has been delivered"
+
+ SUCCESS = "2.0;Success"
+
+ INVALID_CALENDAR_USER = "3.7;Invalid Calendar User"
+ NO_AUTHORITY = "3.8;No authority"
+
+ BAD_REQUEST = "5.0;Service cannot handle request"
+ SERVICE_UNAVAILABLE = "5.1;Service unavailable"
+ INVALID_SERVICE = "5.2;Invalid calendar service"
+ NO_USER_SUPPORT = "5.3;No scheduling support for user"
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,921 +0,0 @@
-##
-# Copyright (c) 2005-2008 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-from twisted.python.failure import Failure
-
-from twisted.web2 import responsecode
-from twisted.web2.dav import davxml
-from twisted.web2.dav.http import errorForFailure, messageForFailure, statusForFailure
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.extensions import ErrorResponse
-from twistedcaldav.ical import Component
-from twistedcaldav.log import Logger, LoggingMixIn
-from twistedcaldav.scheduling import addressmapping
-from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
-from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
- LocalCalendarUser, RemoteCalendarUser, PartitionedCalendarUser
-from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
-from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
-from twistedcaldav.scheduling.itip import iTIPRequestStatus
-
-import itertools
-import re
-import socket
-import urlparse
-
-"""
-CalDAV/Server-to-Server scheduling behavior.
-"""
-
-__all__ = [
- "Scheduler",
- "CalDAVScheduler",
- "IScheduleScheduler",
- "IMIPScheduler",
- "DirectScheduler",
-]
-
-
-log = Logger()
-
-class Scheduler(object):
-
- def __init__(self, request, resource):
- self.request = request
- self.resource = resource
- self.originator = None
- self.recipients = None
- self.calendar = None
- self.calendardata = None
- self.organizer = None
- self.attendee = None
- self.isiTIPRequest = None
- self.timeRange = None
- self.excludeUID = None
- self.fakeTheResult = False
- self.method = "Unknown"
- self.internal_request = False
-
- @inlineCallbacks
- def doSchedulingViaPOST(self, use_request_headers=False):
- """
- The Scheduling POST operation on an Outbox.
- """
-
- self.method = "POST"
-
- # Load various useful bits doing some basic checks on those
- yield self.loadCalendarFromRequest()
-
- if use_request_headers:
- self.loadFromRequestHeaders()
- else:
- yield self.loadFromRequestData()
-
- if not hasattr(self.request, "extendedLogItems"):
- self.request.extendedLogItems = {}
- self.request.extendedLogItems["recipients"] = len(self.recipients)
- self.request.extendedLogItems["cl"] = str(len(self.calendardata))
-
- # Do some extra authorization checks
- self.checkAuthorization()
-
- result = (yield self.doScheduling())
- returnValue(result)
-
- def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
- """
- The implicit scheduling PUT operation.
- """
-
- self.method = "PUT"
-
- # Load various useful bits doing some basic checks on those
- self.originator = originator
- self.recipients = recipients
- self.calendar = calendar
- self.internal_request = internal_request
-
- # Do some extra authorization checks
- self.checkAuthorization()
-
- return self.doScheduling()
-
- @inlineCallbacks
- def doScheduling(self):
- # Check validity of Originator header.
- yield self.checkOriginator()
-
- # Get recipient details.
- yield self.checkRecipients()
-
- # Check calendar data.
- self.checkCalendarData()
-
- # Check validity of ORGANIZER
- yield self.checkOrganizer()
-
- # Do security checks (e.g. spoofing)
- yield self.securityChecks()
-
- # Generate accounting information
- self.doAccounting()
-
- # Do some final checks after we have gathered all our information
- self.finalChecks()
-
- # Do scheduling tasks
- result = (yield self.generateSchedulingResponse())
-
- returnValue(result)
-
- @inlineCallbacks
- def loadFromRequestData(self):
- yield self.loadOriginatorFromRequestDetails()
- self.loadRecipientsFromCalendarData()
-
- @inlineCallbacks
- def loadOriginatorFromRequestDetails(self):
- # Get the originator who is the authenticated user
- originatorPrincipal = None
- originator = ""
- authz_principal = self.resource.currentPrincipal(self.request).children[0]
- if isinstance(authz_principal, davxml.HRef):
- originatorPrincipalURL = str(authz_principal)
- if originatorPrincipalURL:
- originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
- if originatorPrincipal:
- # Pick the first mailto cu address or the first other type
- for item in originatorPrincipal.calendarUserAddresses():
- if not originator:
- originator = item
- if item.startswith("mailto:"):
- originator = item
- break
-
- if not originator:
- log.err("%s request must have Originator" % (self.method,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
- else:
- self.originator = originator
-
- def loadRecipientsFromCalendarData(self):
-
- # Get the ATTENDEEs
- attendees = set()
- for attendee, _ignore in self.calendar.getAttendeesByInstance():
- attendees.add(attendee)
-
- if not attendees:
- log.err("%s request must have at least one Recipient" % (self.method,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
- else:
- self.recipients = list(attendees)
-
- def loadFromRequestHeaders(self):
- """
- Load Originator and Recipient from request headers.
- """
- self.loadOriginatorFromRequestHeaders()
- self.loadRecipientsFromRequestHeaders()
-
- def loadOriginatorFromRequestHeaders(self):
- # Must have Originator header
- originator = self.request.headers.getRawHeaders("originator")
- if originator is None or (len(originator) != 1):
- log.err("%s request must have Originator header" % (self.method,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
- else:
- self.originator = originator[0]
-
- def loadRecipientsFromRequestHeaders(self):
- # Get list of Recipient headers
- rawRecipients = self.request.headers.getRawHeaders("recipient")
- if rawRecipients is None or (len(rawRecipients) == 0):
- log.err("%s request must have at least one Recipient header" % (self.method,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
-
- # Recipient header may be comma separated list
- self.recipients = []
- for rawRecipient in rawRecipients:
- for r in rawRecipient.split(","):
- r = r.strip()
- if len(r):
- self.recipients.append(r)
-
- @inlineCallbacks
- def loadCalendarFromRequest(self):
- # Must be content-type text/calendar
- contentType = self.request.headers.getHeader("content-type")
- if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
- log.err("MIME type %s not allowed in calendar collection" % (contentType,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-
- # Parse the calendar object from the HTTP request stream
- try:
- self.calendar = (yield Component.fromIStream(self.request.stream))
- self.calendardata = str(self.calendar)
- except:
- # FIXME: Bare except
- log.err("Error while handling %s: %s" % (self.method, Failure(),))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Can't parse calendar data"))
-
- def checkAuthorization(self):
- raise NotImplementedError
-
- def checkOriginator(self):
- raise NotImplementedError
-
- def checkRecipients(self):
- raise NotImplementedError
-
- def checkOrganizer(self):
- raise NotImplementedError
-
- def checkOrganizerAsOriginator(self):
- raise NotImplementedError
-
- def checkAttendeeAsOriginator(self):
- raise NotImplementedError
-
- def checkCalendarData(self):
- # Must be a valid calendar
- try:
- self.calendar.validCalendarForCalDAV()
- except ValueError, e:
- log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Calendar component is not valid"))
-
- # Must have a METHOD
- if not self.calendar.isValidMethod():
- log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have valid METHOD property"))
-
- # Verify iTIP behavior
- if not self.calendar.isValidITIP():
- log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have a calendar component that satisfies iTIP requirements"))
-
- # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
- if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
- log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-
- # Determine iTIP method mode
- if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
- self.isiTIPRequest = True
-
- elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
- self.isiTIPRequest = False
-
- # Verify that there is a single ATTENDEE property
- attendees = self.calendar.getAttendees()
-
- # Must have only one
- if len(attendees) != 1:
- log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
- self.attendee = attendees[0]
-
- else:
- msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
- log.err(msg)
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description=msg))
-
- def checkForFreeBusy(self):
- if not hasattr(self, "isfreebusy"):
- if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
- # Extract time range from VFREEBUSY object
- vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
- if len(vfreebusies) != 1:
- log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="iTIP data is not valid for a VFREEBUSY request"))
- dtstart = vfreebusies[0].getStartDateUTC()
- dtend = vfreebusies[0].getEndDateUTC()
- if dtstart is None or dtend is None:
- log.err("VFREEBUSY start/end not valid: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="VFREEBUSY start/end not valid"))
- self.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
- self.timeRange.start = dtstart
- self.timeRange.end = dtend
-
- # Look for masked UID
- self.excludeUID = self.calendar.getMaskUID()
-
- # Do free busy operation
- self.isfreebusy = True
- else:
- # Do regular invite (fan-out)
- self.isfreebusy = False
-
- return self.isfreebusy
-
- def securityChecks(self):
- raise NotImplementedError
-
- def doAccounting(self):
- #
- # Accounting
- #
- # Note that we associate logging with the organizer, not the
- # originator, which is good for looking for why something
- # shows up in a given principal's calendars, rather than
- # tracking the activities of a specific user.
- #
- if isinstance(self.organizer, LocalCalendarUser):
- accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType() == "VFREEBUSY" else "iTIP"
- if accountingEnabled(accountingType, self.organizer.principal):
- emitAccounting(
- accountingType, self.organizer.principal,
- "Originator: %s\nRecipients:\n%sMethod:%s\n\n%s"
- % (
- str(self.originator),
- str("".join([" %s\n" % (recipient,) for recipient in self.recipients])),
- str(self.method),
- self.calendardata
- )
- )
-
- def finalChecks(self):
- """
- Final checks before doing the actual scheduling.
- """
- pass
-
- @inlineCallbacks
- def generateSchedulingResponse(self):
-
- log.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),))
-
- # For free-busy do immediate determination of iTIP result rather than fan-out
- freebusy = self.checkForFreeBusy()
-
- # Prepare for multiple responses
- responses = ScheduleResponseQueue(self.method, responsecode.OK)
-
- # Loop over each recipient and aggregate into lists by service types.
- caldav_recipients = []
- remote_recipients = []
- for recipient in self.recipients:
-
- if self.fakeTheResult:
- responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS if freebusy else iTIPRequestStatus.MESSAGE_DELIVERED)
-
- elif isinstance(recipient, LocalCalendarUser):
- caldav_recipients.append(recipient)
-
- elif isinstance(recipient, PartitionedCalendarUser):
- remote_recipients.append(recipient)
-
- elif isinstance(recipient, RemoteCalendarUser):
- remote_recipients.append(recipient)
-
- else:
- err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
- responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
-
- # Now process local recipients
- if caldav_recipients:
- yield self.generateLocalSchedulingResponses(caldav_recipients, responses, freebusy)
-
- # To reduce chatter, we suppress certain messages
- if not getattr(self.request, 'suppressRefresh', False):
-
- # Now process remote recipients
- if remote_recipients:
- yield self.generateRemoteSchedulingResponses(remote_recipients, responses, freebusy)
-
- # Return with final response if we are done
- returnValue(responses)
-
- def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
- """
- Generate scheduling responses for CalDAV recipients.
- """
-
- # Create the scheduler and run it.
- requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
- return requestor.generateSchedulingResponses()
-
- def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
- """
- Generate scheduling responses for remote recipients.
- """
-
- # Create the scheduler and run it.
- requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
- return requestor.generateSchedulingResponses()
-
-class CalDAVScheduler(Scheduler):
-
- def __init__(self, request, resource):
- super(CalDAVScheduler, self).__init__(request, resource)
- self.doingPOST = False
-
- def doSchedulingViaPOST(self, use_request_headers=False):
- """
- The Scheduling POST operation on an Outbox.
- """
- self.doingPOST = True
- return super(CalDAVScheduler, self).doSchedulingViaPOST(use_request_headers)
-
- def checkAuthorization(self):
- # Must have an authenticated user
- if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
- log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- def checkOriginator(self):
- """
- Check the validity of the Originator header. Extract the corresponding principal.
- """
-
- # Verify that Originator is a valid calendar user
- originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
- if originatorPrincipal is None:
- # Local requests MUST have a principal.
- log.err("Could not find principal for originator: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
- else:
- # Must have a valid Inbox.
- inboxURL = originatorPrincipal.scheduleInboxURL()
- if inboxURL is None:
- log.err("Could not find inbox for originator: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- # Verify that Originator matches the authenticated user, but not if this is a server
- # generated request
- if not self.internal_request:
- authn_principal = self.resource.currentPrincipal(self.request)
- if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != authn_principal:
- log.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
-
- @inlineCallbacks
- def checkRecipients(self):
- """
- Check the validity of the Recipient header values. Map these into local or
- remote CalendarUsers.
- """
-
- results = []
- for recipient in self.recipients:
- # Get the principal resource for this recipient
- principal = self.resource.principalForCalendarUserAddress(recipient)
-
- # If no principal we may have a remote recipient but we should check whether
- # the address is one that ought to be on our server and treat that as a missing
- # user. Also if server-to-server is not enabled then remote addresses are not allowed.
- if principal is None:
- address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
- if isinstance(address, InvalidCalendarUser):
- log.err("Unknown calendar user address: %s" % (recipient,))
- results.append(address)
- else:
- # Map recipient to their inbox
- inbox = None
- inboxURL = principal.scheduleInboxURL()
- if inboxURL:
- inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
- if inbox:
- results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
- else:
- log.err("No schedule inbox for principal: %s" % (principal,))
- results.append(InvalidCalendarUser(recipient))
-
- self.recipients = results
-
- @inlineCallbacks
- def checkOrganizer(self):
- """
- Check the validity of the ORGANIZER value. ORGANIZER must be local.
- """
-
- # Verify that the ORGANIZER's cu address maps to a valid user
- organizer = self.calendar.getOrganizer()
- if organizer:
- organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
- if organizerPrincipal:
- outboxURL = organizerPrincipal.scheduleOutboxURL()
- if outboxURL:
- self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
- else:
- log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
- if localUser:
- log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
- else:
- self.organizer = RemoteCalendarUser(organizer)
- else:
- log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
- def checkOrganizerAsOriginator(self):
-
- # Make sure that the ORGANIZER is local
- if not isinstance(self.organizer, LocalCalendarUser):
- log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
- # Make sure that the ORGANIZER's Outbox is the request URI
- if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
- log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
- def checkAttendeeAsOriginator(self):
- """
- Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
- Only local attendees are allowed for message originating from this server.
- """
-
- # Attendee's Outbox MUST be the request URI
- attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
- if attendeePrincipal:
- if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
- log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
- else:
- log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
- def securityChecks(self):
- """
- Check that the originator has the appropriate rights to send this type of iTIP message.
- """
-
- # Prevent spoofing of ORGANIZER with specific METHODs when local
- if self.isiTIPRequest:
- self.checkOrganizerAsOriginator()
-
- # Prevent spoofing when doing reply-like METHODs
- else:
- self.checkAttendeeAsOriginator()
-
- def finalChecks(self):
- """
- Final checks before doing the actual scheduling.
- """
-
- # With implicit scheduling only certain types of iTIP operations are allowed for POST.
- # This server does not do implicit so we let everything through
- pass
-
-class RemoteScheduler(Scheduler):
-
- def checkOrganizer(self):
- """
- Delay ORGANIZER check until we know what their role is.
- """
- pass
-
- @inlineCallbacks
- def checkRecipients(self):
- """
- Check the validity of the Recipient header values. These must all be local as there
- is no concept of server-to-server relaying.
- """
-
- results = []
- for recipient in self.recipients:
- # Get the principal resource for this recipient
- principal = self.resource.principalForCalendarUserAddress(recipient)
-
- # If no principal we may have a remote recipient but we should check whether
- # the address is one that ought to be on our server and treat that as a missing
- # user. Also if server-to-server is not enabled then remote addresses are not allowed.
- if principal is None:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
- if localUser:
- log.err("No principal for calendar user address: %s" % (recipient,))
- else:
- log.err("Unknown calendar user address: %s" % (recipient,))
- results.append(InvalidCalendarUser(recipient))
- else:
- # Map recipient to their inbox
- inbox = None
- inboxURL = principal.scheduleInboxURL()
- if inboxURL:
- inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
-
- if inbox:
- results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
- else:
- log.err("No schedule inbox for principal: %s" % (principal,))
- results.append(InvalidCalendarUser(recipient))
-
- self.recipients = results
-
-class IScheduleScheduler(RemoteScheduler):
-
- def checkAuthorization(self):
- # Must have an unauthenticated user
- if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
- log.err("Authenticated originators not allowed: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- @inlineCallbacks
- def checkOriginator(self):
- """
- Check the validity of the Originator header.
- """
-
- # For remote requests we do not allow the originator to be a local user or one within our domain.
- originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
- if originatorPrincipal or localUser:
- if originatorPrincipal.locallyHosted():
- log.err("Cannot use originator that is on this server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
- else:
- self.originator = PartitionedCalendarUser(self.originator, originatorPrincipal)
- #self._validPartitionServer()
- else:
- self.originator = RemoteCalendarUser(self.originator)
- self._validiScheduleServer()
-
- def _validiScheduleServer(self):
- """
- Check the validity of the iSchedule host.
- """
-
- # We will only accept originator in known domains.
- servermgr = IScheduleServers()
- server = servermgr.mapDomain(self.originator.domain)
- if not server or not server.allow_from:
- log.err("Originator not on recognized server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
- else:
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
-
- # First compare as dotted IP
- matched = False
- compare_with = (server.host,) + tuple(server.client_hosts)
- if clientip in compare_with:
- matched = True
- else:
- # Now do hostname lookup
- try:
- host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
- for host in itertools.chain((host,), aliases):
- # Try simple match first
- if host in compare_with:
- matched = True
- break
-
- # Try pattern match next
- for pattern in compare_with:
- try:
- if re.match(pattern, host) is not None:
- matched = True
- break
- except re.error:
- log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
- else:
- continue
- break
- except socket.herror, e:
- log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
- if not matched:
- log.err("Originator not on allowed server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- def _validPartitionServer(self, principal):
- """
- Check the validity of the partitioned host.
- """
-
- # Extract expected host/port
- expected_uri = principal.hostedURL()
- expected_uri = urlparse.urlparse(expected_uri)
-
- # Get the request IP and map to hostname.
- clientip = self.request.remoteAddr.host
-
- # First compare as dotted IP
- matched = False
- if clientip == expected_uri.hostname:
- matched = True
- else:
- # Now do hostname lookup
- try:
- host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
- for hostname in itertools.chain((host,), aliases):
- # Try host match
- if hostname == expected_uri.hostname:
- matched = True
- break
- except socket.herror, e:
- log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
-
- if not matched:
- log.err("Originator not on allowed server: %s" % (self.originator,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-
- @inlineCallbacks
- def checkOrganizerAsOriginator(self):
- """
- Check the validity of the ORGANIZER value. ORGANIZER must not be local.
- """
-
- # Verify that the ORGANIZER's cu address does not map to a valid user
- organizer = self.calendar.getOrganizer()
- if organizer:
- organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
- if organizerPrincipal:
- if organizerPrincipal.locallyHosted():
- log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
- else:
- # Check that the origin server is the correct partition
- self.organizer = PartitionedCalendarUser(organizer, organizerPrincipal)
- self._validPartitionServer(self.organizer.principal)
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
- if localUser:
- log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
- else:
- self.organizer = RemoteCalendarUser(organizer)
- else:
- log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
-
- @inlineCallbacks
- def checkAttendeeAsOriginator(self):
- """
- Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
- Only local attendees are allowed for message originating from this server.
- """
-
- # Attendee cannot be local.
- attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
- if attendeePrincipal:
- if attendeePrincipal.locallyHosted():
- log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
- else:
- self._validPartitionServer(attendeePrincipal)
- else:
- localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
- if localUser:
- log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-
- # TODO: in this case we should check that the ORGANIZER is the sole recipient.
-
- @inlineCallbacks
- def securityChecks(self):
- """
- Check that the originator has the appropriate rights to send this type of iTIP message.
- """
-
- # Prevent spoofing of ORGANIZER with specific METHODs when local
- if self.isiTIPRequest:
- yield self.checkOrganizerAsOriginator()
-
- # Prevent spoofing when doing reply-like METHODs
- else:
- yield self.checkAttendeeAsOriginator()
-
-class ScheduleResponseResponse (Response):
- """
- ScheduleResponse L{Response} object.
- Renders itself as a CalDAV:schedule-response XML document.
- """
- def __init__(self, xml_responses, location=None):
- """
- @param xml_responses: an iterable of davxml.Response objects.
- @param location: the value of the location header to return in the response,
- or None.
- """
-
- Response.__init__(self, code=responsecode.OK,
- stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
-
- self.headers.setHeader("content-type", MimeType("text", "xml"))
-
- if location is not None:
- self.headers.setHeader("location", location)
-
-class ScheduleResponseQueue (LoggingMixIn):
- """
- Stores a list of (typically error) responses for use in a
- L{ScheduleResponse}.
- """
- def __init__(self, method, success_response):
- """
- @param method: the name of the method generating the queue.
- @param success_response: the response to return in lieu of a
- L{ScheduleResponse} if no responses are added to this queue.
- """
- self.responses = []
- self.method = method
- self.success_response = success_response
- self.location = None
-
- def setLocation(self, location):
- """
- @param location: the value of the location header to return in the response,
- or None.
- """
- self.location = location
-
- def add(self, recipient, what, reqstatus=None, calendar=None):
- """
- Add a response.
- @param recipient: the recipient for this response.
- @param what: a status code or a L{Failure} for the given recipient.
- @param status: the iTIP request-status for the given recipient.
- @param calendar: the calendar data for the given recipient response.
- """
- if type(what) is int:
- code = what
- error = None
- message = responsecode.RESPONSES[code]
- elif isinstance(what, Failure):
- code = statusForFailure(what)
- error = errorForFailure(what)
- message = messageForFailure(what)
- else:
- raise AssertionError("Unknown data type: %r" % (what,))
-
- if code > 400: # Error codes only
- self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
-
- children = []
- children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
- children.append(caldavxml.RequestStatus(reqstatus))
- if calendar is not None:
- children.append(caldavxml.CalendarData.fromCalendar(calendar))
- if error is not None:
- children.append(error)
- if message is not None:
- children.append(davxml.ResponseDescription(message))
- self.responses.append(caldavxml.Response(*children))
-
- def clone(self, clone):
- """
- Add a response cloned from an existing caldavxml.Response element.
- @param clone: the response to clone.
- """
- if not isinstance(clone, caldavxml.Response):
- raise AssertionError("Incorrect element type: %r" % (clone,))
-
- recipient = clone.childOfType(caldavxml.Recipient)
- request_status = clone.childOfType(caldavxml.RequestStatus)
- calendar_data = clone.childOfType(caldavxml.CalendarData)
- error = clone.childOfType(davxml.Error)
- desc = clone.childOfType(davxml.ResponseDescription)
-
- children = []
- children.append(recipient)
- children.append(request_status)
- if calendar_data is not None:
- children.append(calendar_data)
- if error is not None:
- children.append(error)
- if desc is not None:
- children.append(desc)
- self.responses.append(caldavxml.Response(*children))
-
- def response(self):
- """
- Generate a L{ScheduleResponseResponse} with the responses contained in the
- queue or, if no such responses, return the C{success_response} provided
- to L{__init__}.
- @return: the response.
- """
- if self.responses:
- return ScheduleResponseResponse(self.responses, self.location)
- else:
- return self.success_response
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/scheduler.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/scheduler.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,921 @@
+##
+# Copyright (c) 2005-2008 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twisted.python.failure import Failure
+
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.extensions import ErrorResponse
+from twistedcaldav.ical import Component
+from twistedcaldav.log import Logger, LoggingMixIn
+from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
+ LocalCalendarUser, RemoteCalendarUser, PartitionedCalendarUser
+from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+from twistedcaldav.scheduling.itip import iTIPRequestStatus
+
+import itertools
+import re
+import socket
+import urlparse
+
+"""
+CalDAV/Server-to-Server scheduling behavior.
+"""
+
+__all__ = [
+ "Scheduler",
+ "CalDAVScheduler",
+ "IScheduleScheduler",
+ "IMIPScheduler",
+ "DirectScheduler",
+]
+
+
+log = Logger()
+
+class Scheduler(object):
+
+ def __init__(self, request, resource):
+ self.request = request
+ self.resource = resource
+ self.originator = None
+ self.recipients = None
+ self.calendar = None
+ self.calendardata = None
+ self.organizer = None
+ self.attendee = None
+ self.isiTIPRequest = None
+ self.timeRange = None
+ self.excludeUID = None
+ self.fakeTheResult = False
+ self.method = "Unknown"
+ self.internal_request = False
+
+ @inlineCallbacks
+ def doSchedulingViaPOST(self, use_request_headers=False):
+ """
+ The Scheduling POST operation on an Outbox.
+ """
+
+ self.method = "POST"
+
+ # Load various useful bits doing some basic checks on those
+ yield self.loadCalendarFromRequest()
+
+ if use_request_headers:
+ self.loadFromRequestHeaders()
+ else:
+ yield self.loadFromRequestData()
+
+ if not hasattr(self.request, "extendedLogItems"):
+ self.request.extendedLogItems = {}
+ self.request.extendedLogItems["recipients"] = len(self.recipients)
+ self.request.extendedLogItems["cl"] = str(len(self.calendardata))
+
+ # Do some extra authorization checks
+ self.checkAuthorization()
+
+ result = (yield self.doScheduling())
+ returnValue(result)
+
+ def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
+ """
+ The implicit scheduling PUT operation.
+ """
+
+ self.method = "PUT"
+
+ # Load various useful bits doing some basic checks on those
+ self.originator = originator
+ self.recipients = recipients
+ self.calendar = calendar
+ self.internal_request = internal_request
+
+ # Do some extra authorization checks
+ self.checkAuthorization()
+
+ return self.doScheduling()
+
+ @inlineCallbacks
+ def doScheduling(self):
+ # Check validity of Originator header.
+ yield self.checkOriginator()
+
+ # Get recipient details.
+ yield self.checkRecipients()
+
+ # Check calendar data.
+ self.checkCalendarData()
+
+ # Check validity of ORGANIZER
+ yield self.checkOrganizer()
+
+ # Do security checks (e.g. spoofing)
+ yield self.securityChecks()
+
+ # Generate accounting information
+ self.doAccounting()
+
+ # Do some final checks after we have gathered all our information
+ self.finalChecks()
+
+ # Do scheduling tasks
+ result = (yield self.generateSchedulingResponse())
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def loadFromRequestData(self):
+ yield self.loadOriginatorFromRequestDetails()
+ self.loadRecipientsFromCalendarData()
+
+ @inlineCallbacks
+ def loadOriginatorFromRequestDetails(self):
+ # Get the originator who is the authenticated user
+ originatorPrincipal = None
+ originator = ""
+ authz_principal = self.resource.currentPrincipal(self.request).children[0]
+ if isinstance(authz_principal, davxml.HRef):
+ originatorPrincipalURL = str(authz_principal)
+ if originatorPrincipalURL:
+ originatorPrincipal = (yield self.request.locateResource(originatorPrincipalURL))
+ if originatorPrincipal:
+ # Pick the first mailto cu address or the first other type
+ for item in originatorPrincipal.calendarUserAddresses():
+ if not originator:
+ originator = item
+ if item.startswith("mailto:"):
+ originator = item
+ break
+
+ if not originator:
+ log.err("%s request must have Originator" % (self.method,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+ else:
+ self.originator = originator
+
+ def loadRecipientsFromCalendarData(self):
+
+ # Get the ATTENDEEs
+ attendees = set()
+ for attendee, _ignore in self.calendar.getAttendeesByInstance():
+ attendees.add(attendee)
+
+ if not attendees:
+ log.err("%s request must have at least one Recipient" % (self.method,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+ else:
+ self.recipients = list(attendees)
+
+ def loadFromRequestHeaders(self):
+ """
+ Load Originator and Recipient from request headers.
+ """
+ self.loadOriginatorFromRequestHeaders()
+ self.loadRecipientsFromRequestHeaders()
+
+ def loadOriginatorFromRequestHeaders(self):
+ # Must have Originator header
+ originator = self.request.headers.getRawHeaders("originator")
+ if originator is None or (len(originator) != 1):
+ log.err("%s request must have Originator header" % (self.method,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+ else:
+ self.originator = originator[0]
+
+ def loadRecipientsFromRequestHeaders(self):
+ # Get list of Recipient headers
+ rawRecipients = self.request.headers.getRawHeaders("recipient")
+ if rawRecipients is None or (len(rawRecipients) == 0):
+ log.err("%s request must have at least one Recipient header" % (self.method,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+
+ # Recipient header may be comma separated list
+ self.recipients = []
+ for rawRecipient in rawRecipients:
+ for r in rawRecipient.split(","):
+ r = r.strip()
+ if len(r):
+ self.recipients.append(r)
+
+ @inlineCallbacks
+ def loadCalendarFromRequest(self):
+ # Must be content-type text/calendar
+ contentType = self.request.headers.getHeader("content-type")
+ if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
+ log.err("MIME type %s not allowed in calendar collection" % (contentType,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+
+ # Parse the calendar object from the HTTP request stream
+ try:
+ self.calendar = (yield Component.fromIStream(self.request.stream))
+ self.calendardata = str(self.calendar)
+ except:
+ # FIXME: Bare except
+ log.err("Error while handling %s: %s" % (self.method, Failure(),))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Can't parse calendar data"))
+
+ def checkAuthorization(self):
+ raise NotImplementedError
+
+ def checkOriginator(self):
+ raise NotImplementedError
+
+ def checkRecipients(self):
+ raise NotImplementedError
+
+ def checkOrganizer(self):
+ raise NotImplementedError
+
+ def checkOrganizerAsOriginator(self):
+ raise NotImplementedError
+
+ def checkAttendeeAsOriginator(self):
+ raise NotImplementedError
+
+ def checkCalendarData(self):
+ # Must be a valid calendar
+ try:
+ self.calendar.validCalendarForCalDAV()
+ except ValueError, e:
+ log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Calendar component is not valid"))
+
+ # Must have a METHOD
+ if not self.calendar.isValidMethod():
+ log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have valid METHOD property"))
+
+ # Verify iTIP behavior
+ if not self.calendar.isValidITIP():
+ log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Must have a calendar component that satisfies iTIP requirements"))
+
+ # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
+ if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
+ log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component %s request: %s" % (self.method, self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
+
+ # Determine iTIP method mode
+ if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+ self.isiTIPRequest = True
+
+ elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+ self.isiTIPRequest = False
+
+ # Verify that there is a single ATTENDEE property
+ attendees = self.calendar.getAttendees()
+
+ # Must have only one
+ if len(attendees) != 1:
+ log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ self.attendee = attendees[0]
+
+ else:
+ msg = "Unknown iTIP METHOD: %s" % (self.calendar.propertyValue("METHOD"),)
+ log.err(msg)
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description=msg))
+
+ def checkForFreeBusy(self):
+ if not hasattr(self, "isfreebusy"):
+ if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
+ # Extract time range from VFREEBUSY object
+ vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
+ if len(vfreebusies) != 1:
+ log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="iTIP data is not valid for a VFREEBUSY request"))
+ dtstart = vfreebusies[0].getStartDateUTC()
+ dtend = vfreebusies[0].getEndDateUTC()
+ if dtstart is None or dtend is None:
+ log.err("VFREEBUSY start/end not valid: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="VFREEBUSY start/end not valid"))
+ self.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
+ self.timeRange.start = dtstart
+ self.timeRange.end = dtend
+
+ # Look for masked UID
+ self.excludeUID = self.calendar.getMaskUID()
+
+ # Do free busy operation
+ self.isfreebusy = True
+ else:
+ # Do regular invite (fan-out)
+ self.isfreebusy = False
+
+ return self.isfreebusy
+
+ def securityChecks(self):
+ raise NotImplementedError
+
+ def doAccounting(self):
+ #
+ # Accounting
+ #
+ # Note that we associate logging with the organizer, not the
+ # originator, which is good for looking for why something
+ # shows up in a given principal's calendars, rather than
+ # tracking the activities of a specific user.
+ #
+ if isinstance(self.organizer, LocalCalendarUser):
+ accountingType = "iTIP-VFREEBUSY" if self.calendar.mainType() == "VFREEBUSY" else "iTIP"
+ if accountingEnabled(accountingType, self.organizer.principal):
+ emitAccounting(
+ accountingType, self.organizer.principal,
+ "Originator: %s\nRecipients:\n%sMethod:%s\n\n%s"
+ % (
+ str(self.originator),
+ str("".join([" %s\n" % (recipient,) for recipient in self.recipients])),
+ str(self.method),
+ self.calendardata
+ )
+ )
+
+ def finalChecks(self):
+ """
+ Final checks before doing the actual scheduling.
+ """
+ pass
+
+ @inlineCallbacks
+ def generateSchedulingResponse(self):
+
+ log.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),))
+
+ # For free-busy do immediate determination of iTIP result rather than fan-out
+ freebusy = self.checkForFreeBusy()
+
+ # Prepare for multiple responses
+ responses = ScheduleResponseQueue(self.method, responsecode.OK)
+
+ # Loop over each recipient and aggregate into lists by service types.
+ caldav_recipients = []
+ remote_recipients = []
+ for recipient in self.recipients:
+
+ if self.fakeTheResult:
+ responses.add(recipient.cuaddr, responsecode.OK, reqstatus=iTIPRequestStatus.SUCCESS if freebusy else iTIPRequestStatus.MESSAGE_DELIVERED)
+
+ elif isinstance(recipient, LocalCalendarUser):
+ caldav_recipients.append(recipient)
+
+ elif isinstance(recipient, PartitionedCalendarUser):
+ remote_recipients.append(recipient)
+
+ elif isinstance(recipient, RemoteCalendarUser):
+ remote_recipients.append(recipient)
+
+ else:
+ err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+ responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus=iTIPRequestStatus.INVALID_CALENDAR_USER)
+
+ # Now process local recipients
+ if caldav_recipients:
+ yield self.generateLocalSchedulingResponses(caldav_recipients, responses, freebusy)
+
+ # To reduce chatter, we suppress certain messages
+ if not getattr(self.request, 'suppressRefresh', False):
+
+ # Now process remote recipients
+ if remote_recipients:
+ yield self.generateRemoteSchedulingResponses(remote_recipients, responses, freebusy)
+
+ # Return with final response if we are done
+ returnValue(responses)
+
+ def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
+ """
+ Generate scheduling responses for CalDAV recipients.
+ """
+
+ # Create the scheduler and run it.
+ requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
+ return requestor.generateSchedulingResponses()
+
+ def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
+ """
+ Generate scheduling responses for remote recipients.
+ """
+
+ # Create the scheduler and run it.
+ requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
+ return requestor.generateSchedulingResponses()
+
+class CalDAVScheduler(Scheduler):
+
+ def __init__(self, request, resource):
+ super(CalDAVScheduler, self).__init__(request, resource)
+ self.doingPOST = False
+
+ def doSchedulingViaPOST(self, use_request_headers=False):
+ """
+ The Scheduling POST operation on an Outbox.
+ """
+ self.doingPOST = True
+ return super(CalDAVScheduler, self).doSchedulingViaPOST(use_request_headers)
+
+ def checkAuthorization(self):
+ # Must have an authenticated user
+ if not self.internal_request and self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
+ log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ def checkOriginator(self):
+ """
+ Check the validity of the Originator header. Extract the corresponding principal.
+ """
+
+ # Verify that Originator is a valid calendar user
+ originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+ if originatorPrincipal is None:
+ # Local requests MUST have a principal.
+ log.err("Could not find principal for originator: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ else:
+ # Must have a valid Inbox.
+ inboxURL = originatorPrincipal.scheduleInboxURL()
+ if inboxURL is None:
+ log.err("Could not find inbox for originator: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ # Verify that Originator matches the authenticated user, but not if this is a server
+ # generated request
+ if not self.internal_request:
+ authn_principal = self.resource.currentPrincipal(self.request)
+ if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != authn_principal:
+ log.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ self.originator = LocalCalendarUser(self.originator, originatorPrincipal)
+
+ @inlineCallbacks
+ def checkRecipients(self):
+ """
+ Check the validity of the Recipient header values. Map these into local or
+ remote CalendarUsers.
+ """
+
+ results = []
+ for recipient in self.recipients:
+ # Get the principal resource for this recipient
+ principal = self.resource.principalForCalendarUserAddress(recipient)
+
+ # If no principal we may have a remote recipient but we should check whether
+ # the address is one that ought to be on our server and treat that as a missing
+ # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+ if principal is None:
+ address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
+ if isinstance(address, InvalidCalendarUser):
+ log.err("Unknown calendar user address: %s" % (recipient,))
+ results.append(address)
+ else:
+ # Map recipient to their inbox
+ inbox = None
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL:
+ inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
+
+ if inbox:
+ results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
+ else:
+ log.err("No schedule inbox for principal: %s" % (principal,))
+ results.append(InvalidCalendarUser(recipient))
+
+ self.recipients = results
+
+ @inlineCallbacks
+ def checkOrganizer(self):
+ """
+ Check the validity of the ORGANIZER value. ORGANIZER must be local.
+ """
+
+ # Verify that the ORGANIZER's cu address maps to a valid user
+ organizer = self.calendar.getOrganizer()
+ if organizer:
+ organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+ if organizerPrincipal:
+ outboxURL = organizerPrincipal.scheduleOutboxURL()
+ if outboxURL:
+ self.organizer = LocalCalendarUser(organizer, organizerPrincipal)
+ else:
+ log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ else:
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+ if localUser:
+ log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ else:
+ self.organizer = RemoteCalendarUser(organizer)
+ else:
+ log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ def checkOrganizerAsOriginator(self):
+
+ # Make sure that the ORGANIZER is local
+ if not isinstance(self.organizer, LocalCalendarUser):
+ log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ # Make sure that the ORGANIZER's Outbox is the request URI
+ if self.doingPOST and self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+ log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ def checkAttendeeAsOriginator(self):
+ """
+ Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+ Only local attendees are allowed for message originating from this server.
+ """
+
+ # Attendee's Outbox MUST be the request URI
+ attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+ if attendeePrincipal:
+ if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != self.request.uri:
+ log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ else:
+ log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ def securityChecks(self):
+ """
+ Check that the originator has the appropriate rights to send this type of iTIP message.
+ """
+
+ # Prevent spoofing of ORGANIZER with specific METHODs when local
+ if self.isiTIPRequest:
+ self.checkOrganizerAsOriginator()
+
+ # Prevent spoofing when doing reply-like METHODs
+ else:
+ self.checkAttendeeAsOriginator()
+
+ def finalChecks(self):
+ """
+ Final checks before doing the actual scheduling.
+ """
+
+ # With implicit scheduling only certain types of iTIP operations are allowed for POST.
+ # This server does not do implicit so we let everything through
+ pass
+
+class RemoteScheduler(Scheduler):
+
+ def checkOrganizer(self):
+ """
+ Delay ORGANIZER check until we know what their role is.
+ """
+ pass
+
+ @inlineCallbacks
+ def checkRecipients(self):
+ """
+ Check the validity of the Recipient header values. These must all be local as there
+ is no concept of server-to-server relaying.
+ """
+
+ results = []
+ for recipient in self.recipients:
+ # Get the principal resource for this recipient
+ principal = self.resource.principalForCalendarUserAddress(recipient)
+
+ # If no principal we may have a remote recipient but we should check whether
+ # the address is one that ought to be on our server and treat that as a missing
+ # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+ if principal is None:
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
+ if localUser:
+ log.err("No principal for calendar user address: %s" % (recipient,))
+ else:
+ log.err("Unknown calendar user address: %s" % (recipient,))
+ results.append(InvalidCalendarUser(recipient))
+ else:
+ # Map recipient to their inbox
+ inbox = None
+ inboxURL = principal.scheduleInboxURL()
+ if inboxURL:
+ inbox = (yield self.request.locateResource(inboxURL)) if principal.locallyHosted() else "dummy"
+
+ if inbox:
+ results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL) if principal.locallyHosted() else PartitionedCalendarUser(recipient, principal))
+ else:
+ log.err("No schedule inbox for principal: %s" % (principal,))
+ results.append(InvalidCalendarUser(recipient))
+
+ self.recipients = results
+
+class IScheduleScheduler(RemoteScheduler):
+
+ def checkAuthorization(self):
+ # Must have an unauthenticated user
+ if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+ log.err("Authenticated originators not allowed: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ @inlineCallbacks
+ def checkOriginator(self):
+ """
+ Check the validity of the Originator header.
+ """
+
+ # For remote requests we do not allow the originator to be a local user or one within our domain.
+ originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+ if originatorPrincipal or localUser:
+ if originatorPrincipal.locallyHosted():
+ log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ else:
+ self.originator = PartitionedCalendarUser(self.originator, originatorPrincipal)
+ #self._validPartitionServer()
+ else:
+ self.originator = RemoteCalendarUser(self.originator)
+ self._validiScheduleServer()
+
+ def _validiScheduleServer(self):
+ """
+ Check the validity of the iSchedule host.
+ """
+
+ # We will only accept originator in known domains.
+ servermgr = IScheduleServers()
+ server = servermgr.mapDomain(self.originator.domain)
+ if not server or not server.allow_from:
+ log.err("Originator not on recognized server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+ else:
+ # Get the request IP and map to hostname.
+ clientip = self.request.remoteAddr.host
+
+ # First compare as dotted IP
+ matched = False
+ compare_with = (server.host,) + tuple(server.client_hosts)
+ if clientip in compare_with:
+ matched = True
+ else:
+ # Now do hostname lookup
+ try:
+ host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+ for host in itertools.chain((host,), aliases):
+ # Try simple match first
+ if host in compare_with:
+ matched = True
+ break
+
+ # Try pattern match next
+ for pattern in compare_with:
+ try:
+ if re.match(pattern, host) is not None:
+ matched = True
+ break
+ except re.error:
+ log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
+ else:
+ continue
+ break
+ except socket.herror, e:
+ log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+
+ if not matched:
+ log.err("Originator not on allowed server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ def _validPartitionServer(self, principal):
+ """
+ Check the validity of the partitioned host.
+ """
+
+ # Extract expected host/port
+ expected_uri = principal.hostedURL()
+ expected_uri = urlparse.urlparse(expected_uri)
+
+ # Get the request IP and map to hostname.
+ clientip = self.request.remoteAddr.host
+
+ # First compare as dotted IP
+ matched = False
+ if clientip == expected_uri.hostname:
+ matched = True
+ else:
+ # Now do hostname lookup
+ try:
+ host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+ for hostname in itertools.chain((host,), aliases):
+ # Try host match
+ if hostname == expected_uri.hostname:
+ matched = True
+ break
+ except socket.herror, e:
+ log.debug("iSchedule cannot lookup client ip '%s': %s" % (clientip, str(e),))
+
+ if not matched:
+ log.err("Originator not on allowed server: %s" % (self.originator,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+ @inlineCallbacks
+ def checkOrganizerAsOriginator(self):
+ """
+ Check the validity of the ORGANIZER value. ORGANIZER must not be local.
+ """
+
+ # Verify that the ORGANIZER's cu address does not map to a valid user
+ organizer = self.calendar.getOrganizer()
+ if organizer:
+ organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+ if organizerPrincipal:
+ if organizerPrincipal.locallyHosted():
+ log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ else:
+ # Check that the origin server is the correct partition
+ self.organizer = PartitionedCalendarUser(organizer, organizerPrincipal)
+ self._validPartitionServer(self.organizer.principal)
+ else:
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+ if localUser:
+ log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+ else:
+ self.organizer = RemoteCalendarUser(organizer)
+ else:
+ log.err("ORGANIZER missing in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+ @inlineCallbacks
+ def checkAttendeeAsOriginator(self):
+ """
+ Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+ Only local attendees are allowed for message originating from this server.
+ """
+
+ # Attendee cannot be local.
+ attendeePrincipal = self.resource.principalForCalendarUserAddress(self.attendee)
+ if attendeePrincipal:
+ if attendeePrincipal.locallyHosted():
+ log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+ else:
+ self._validPartitionServer(attendeePrincipal)
+ else:
+ localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.attendee))
+ if localUser:
+ log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendardata,))
+ raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+
+ # TODO: in this case we should check that the ORGANIZER is the sole recipient.
+
+ @inlineCallbacks
+ def securityChecks(self):
+ """
+ Check that the originator has the appropriate rights to send this type of iTIP message.
+ """
+
+ # Prevent spoofing of ORGANIZER with specific METHODs when local
+ if self.isiTIPRequest:
+ yield self.checkOrganizerAsOriginator()
+
+ # Prevent spoofing when doing reply-like METHODs
+ else:
+ yield self.checkAttendeeAsOriginator()
+
+class ScheduleResponseResponse (Response):
+ """
+ ScheduleResponse L{Response} object.
+ Renders itself as a CalDAV:schedule-response XML document.
+ """
+ def __init__(self, xml_responses, location=None):
+ """
+ @param xml_responses: an iterable of davxml.Response objects.
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+
+ Response.__init__(self, code=responsecode.OK,
+ stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+
+ self.headers.setHeader("content-type", MimeType("text", "xml"))
+
+ if location is not None:
+ self.headers.setHeader("location", location)
+
+class ScheduleResponseQueue (LoggingMixIn):
+ """
+ Stores a list of (typically error) responses for use in a
+ L{ScheduleResponse}.
+ """
+ def __init__(self, method, success_response):
+ """
+ @param method: the name of the method generating the queue.
+ @param success_response: the response to return in lieu of a
+ L{ScheduleResponse} if no responses are added to this queue.
+ """
+ self.responses = []
+ self.method = method
+ self.success_response = success_response
+ self.location = None
+
+ def setLocation(self, location):
+ """
+ @param location: the value of the location header to return in the response,
+ or None.
+ """
+ self.location = location
+
+ def add(self, recipient, what, reqstatus=None, calendar=None):
+ """
+ Add a response.
+ @param recipient: the recipient for this response.
+ @param what: a status code or a L{Failure} for the given recipient.
+ @param status: the iTIP request-status for the given recipient.
+ @param calendar: the calendar data for the given recipient response.
+ """
+ if type(what) is int:
+ code = what
+ error = None
+ message = responsecode.RESPONSES[code]
+ elif isinstance(what, Failure):
+ code = statusForFailure(what)
+ error = errorForFailure(what)
+ message = messageForFailure(what)
+ else:
+ raise AssertionError("Unknown data type: %r" % (what,))
+
+ if code > 400: # Error codes only
+ self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
+
+ children = []
+ children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
+ children.append(caldavxml.RequestStatus(reqstatus))
+ if calendar is not None:
+ children.append(caldavxml.CalendarData.fromCalendar(calendar))
+ if error is not None:
+ children.append(error)
+ if message is not None:
+ children.append(davxml.ResponseDescription(message))
+ self.responses.append(caldavxml.Response(*children))
+
+ def clone(self, clone):
+ """
+ Add a response cloned from an existing caldavxml.Response element.
+ @param clone: the response to clone.
+ """
+ if not isinstance(clone, caldavxml.Response):
+ raise AssertionError("Incorrect element type: %r" % (clone,))
+
+ recipient = clone.childOfType(caldavxml.Recipient)
+ request_status = clone.childOfType(caldavxml.RequestStatus)
+ calendar_data = clone.childOfType(caldavxml.CalendarData)
+ error = clone.childOfType(davxml.Error)
+ desc = clone.childOfType(davxml.ResponseDescription)
+
+ children = []
+ children.append(recipient)
+ children.append(request_status)
+ if calendar_data is not None:
+ children.append(calendar_data)
+ if error is not None:
+ children.append(error)
+ if desc is not None:
+ children.append(desc)
+ self.responses.append(caldavxml.Response(*children))
+
+ def response(self):
+ """
+ Generate a L{ScheduleResponseResponse} with the responses contained in the
+ queue or, if no such responses, return the C{success_response} provided
+ to L{__init__}.
+ @return: the response.
+ """
+ if self.responses:
+ return ScheduleResponseResponse(self.responses, self.location)
+ else:
+ return self.success_response
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/test/__init__.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Tests for the twistedcaldav.scheduling module.
-"""
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/test/__init__.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Tests for the twistedcaldav.scheduling module.
+"""
Deleted: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/utils.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -1,58 +0,0 @@
-#
-# 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.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.internet.defer import inlineCallbacks, succeed, returnValue
-from twistedcaldav.method import report_common
-
- at inlineCallbacks
-def getCalendarObjectForPrincipals(request, principal, uid):
- """
- Get a copy of the event for a principal.
- """
-
- result = {
- "resource": None,
- "resource_name": None,
- "calendar_collection": None,
- "calendar_collection_uri": None,
- }
-
- if principal and principal.locallyHosted():
- # Get principal's calendar-home
- calendar_home = principal.calendarHome()
-
- # FIXME: because of the URL->resource request mapping thing, we have to force the request
- # to recognize this resource
- request._rememberResource(calendar_home, calendar_home.url())
-
- # Run a UID query against the UID
-
- def queryCalendarCollection(collection, uri):
- rname = collection.index().resourceNameForUID(uid)
- if rname:
- result["resource"] = collection.getChild(rname)
- result["resource_name"] = rname
- result["calendar_collection"] = collection
- result["calendar_collection_uri"] = uri
- return succeed(False)
- else:
- return succeed(True)
-
- # NB We are by-passing privilege checking here. That should be OK as the data found is not
- # exposed to the user.
- yield report_common.applyToCalendarCollections(calendar_home, request, calendar_home.url(), "infinity", queryCalendarCollection, None)
-
- returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/scheduling/utils.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/scheduling/utils.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,58 @@
+#
+# 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
+from twistedcaldav.method import report_common
+
+ at inlineCallbacks
+def getCalendarObjectForPrincipals(request, principal, uid):
+ """
+ Get a copy of the event for a principal.
+ """
+
+ result = {
+ "resource": None,
+ "resource_name": None,
+ "calendar_collection": None,
+ "calendar_collection_uri": None,
+ }
+
+ if principal and principal.locallyHosted():
+ # Get principal's calendar-home
+ calendar_home = principal.calendarHome()
+
+ # FIXME: because of the URL->resource request mapping thing, we have to force the request
+ # to recognize this resource
+ request._rememberResource(calendar_home, calendar_home.url())
+
+ # Run a UID query against the UID
+
+ def queryCalendarCollection(collection, uri):
+ rname = collection.index().resourceNameForUID(uid)
+ if rname:
+ result["resource"] = collection.getChild(rname)
+ result["resource_name"] = rname
+ result["calendar_collection"] = collection
+ result["calendar_collection_uri"] = uri
+ return succeed(False)
+ else:
+ return succeed(True)
+
+ # NB We are by-passing privilege checking here. That should be OK as the data found is not
+ # exposed to the user.
+ yield report_common.applyToCalendarCollections(calendar_home, request, calendar_home.url(), "infinity", queryCalendarCollection, None)
+
+ returnValue((result["resource"], result["resource_name"], result["calendar_collection"], result["calendar_collection_uri"],))
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/static.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/static.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -49,11 +49,12 @@
from twisted.web2.dav.idav import IDAVResource
from twisted.web2.dav.resource import AccessDeniedError
from twisted.web2.dav.resource import davPrivilegeSet
-from twisted.web2.dav.util import parentForURL, bindMethods
+from twisted.web2.dav.util import parentForURL, bindMethods, joinURL
from twistedcaldav import caldavxml
from twistedcaldav import customxml
from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.client.reverseproxy import ReverseProxyResource
from twistedcaldav.config import config
from twistedcaldav.customxml import TwistedCalendarAccessProperty
from twistedcaldav.extensions import DAVFile
@@ -63,7 +64,8 @@
from twistedcaldav.ical import Property as iProperty
from twistedcaldav.index import Index, IndexSchedule
from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource,\
+ IScheduleInboxResource
from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
from twistedcaldav.directory.calendar import uidsResourceName
from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -341,7 +343,7 @@
if isCalendarCollectionResource(self):
- # Short-circuit stat with information we know to be true at this point
+ # Short-circuit stat with information we know to be true at this point
if isinstance(path, FilePath) and hasattr(self, "knownChildren"):
if os.path.basename(path.path) in self.knownChildren:
path.existsCached = True
@@ -612,60 +614,77 @@
assert len(name) > 4, "Directory record has an invalid GUID: %r" % (name,)
- childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
- child = self.homeResourceClass(childPath.path, self, record)
-
- if not child.exists():
- self.provision()
-
- if not childPath.parent().isdir():
- childPath.parent().makedirs()
-
- for oldPath in (
- # Pre 2.0: All in one directory
- self.fp.child(name),
- # Pre 1.2: In types hierarchy instead of the GUID hierarchy
- self.parent.getChild(record.recordType).fp.child(record.shortName),
- ):
- if oldPath.exists():
- # The child exists at an old location. Move to new location.
- log.msg("Moving calendar home from old location %r to new location %r." % (oldPath, childPath))
- try:
- oldPath.moveTo(childPath)
- except (OSError, IOError), e:
- log.err("Error moving calendar home %r: %s" % (oldPath, e))
+ if record.locallyHosted():
+ childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+ child = self.homeResourceClass(childPath.path, self, record)
+
+ if not child.exists():
+ self.provision()
+
+ if not childPath.parent().isdir():
+ childPath.parent().makedirs()
+
+ for oldPath in (
+ # Pre 2.0: All in one directory
+ self.fp.child(name),
+ # Pre 1.2: In types hierarchy instead of the GUID hierarchy
+ self.parent.getChild(record.recordType).fp.child(record.shortName),
+ ):
+ if oldPath.exists():
+ # The child exists at an old location. Move to new location.
+ log.msg("Moving calendar home from old location %r to new location %r." % (oldPath, childPath))
+ try:
+ oldPath.moveTo(childPath)
+ except (OSError, IOError), e:
+ log.err("Error moving calendar home %r: %s" % (oldPath, e))
+ raise HTTPError(StatusResponse(
+ responsecode.INTERNAL_SERVER_ERROR,
+ "Unable to move calendar home."
+ ))
+ child.fp.changed()
+ break
+ else:
+ #
+ # NOTE: provisionDefaultCalendars() returns a deferred, which we are ignoring.
+ # The result being that the default calendars will be present at some point
+ # in the future, not necessarily right now, and we don't have a way to wait
+ # on that to finish.
+ #
+ child.provisionDefaultCalendars()
+
+ #
+ # Try to work around the above a little by telling the client that something
+ # when wrong temporarily if the child isn't provisioned right away.
+ #
+ if not child.exists():
raise HTTPError(StatusResponse(
- responsecode.INTERNAL_SERVER_ERROR,
- "Unable to move calendar home."
+ responsecode.SERVICE_UNAVAILABLE,
+ "Provisioning calendar home."
))
- child.fp.changed()
- break
- else:
- #
- # NOTE: provisionDefaultCalendars() returns a deferred, which we are ignoring.
- # The result being that the default calendars will be present at some point
- # in the future, not necessarily right now, and we don't have a way to wait
- # on that to finish.
- #
- child.provisionDefaultCalendars()
+
+ assert child.exists()
+
+ else:
+ childPath = self.fp.child(name[0:2]).child(name[2:4]).child(name)
+ child = CalendarHomeReverseProxyFile(childPath.path, self, record)
- #
- # Try to work around the above a little by telling the client that something
- # when wrong temporarily if the child isn't provisioned right away.
- #
- if not child.exists():
- raise HTTPError(StatusResponse(
- responsecode.SERVICE_UNAVAILABLE,
- "Provisioning calendar home."
- ))
-
- assert child.exists()
-
return child
def createSimilarFile(self, path):
raise HTTPError(responsecode.NOT_FOUND)
+class CalendarHomeReverseProxyFile(ReverseProxyResource):
+
+ def __init__(self, path, parent, record):
+ self.path = path
+ self.parent = parent
+ self.record = record
+
+ super(CalendarHomeReverseProxyFile, self).__init__(self.record.hostedAt)
+
+ def url(self):
+ return joinURL(self.parent.url(), self.record.guid)
+
class CalendarHomeFile (PropfindCacheMixin, AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
"""
Calendar home collection resource.
@@ -858,6 +877,51 @@
def __repr__(self):
return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
+class IScheduleInboxFile (IScheduleInboxResource, CalDAVFile):
+ """
+ Server-to-server scheduling inbox resource.
+ """
+ def __init__(self, path, parent):
+ CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+ IScheduleInboxResource.__init__(self, parent)
+
+ def __repr__(self):
+ return "<%s (server-to-server inbox resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+ def isCollection(self):
+ return False
+
+ def createSimilarFile(self, path):
+ if path == self.fp.path:
+ return self
+ else:
+ return responsecode.NOT_FOUND
+
+ def http_PUT (self, request): return responsecode.FORBIDDEN
+ def http_COPY (self, request): return responsecode.FORBIDDEN
+ def http_MOVE (self, request): return responsecode.FORBIDDEN
+ def http_DELETE (self, request): return responsecode.FORBIDDEN
+ def http_MKCOL (self, request): return responsecode.FORBIDDEN
+
+ def http_MKCALENDAR(self, request):
+ return ErrorResponse(
+ responsecode.FORBIDDEN,
+ (caldav_namespace, "calendar-collection-location-ok")
+ )
+
+ def etag(self):
+ return None
+
+ def checkPreconditions(self, request):
+ return None
+
+ ##
+ # ACL
+ ##
+
+ def supportedPrivileges(self, request):
+ return succeed(schedulePrivilegeSet)
+
class DropBoxHomeFile (AutoProvisioningFileMixIn, DropBoxHomeResource, CalDAVFile):
def __init__(self, path, parent):
DropBoxHomeResource.__init__(self)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/tap.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/tap.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -21,6 +21,7 @@
from zope.interface import implements
+from twisted.internet import reactor
from twisted.internet.address import IPv4Address
from twisted.internet import tcp, ssl
@@ -48,12 +49,15 @@
from twistedcaldav.config import config, parseConfig, defaultConfig, ConfigurationError
from twistedcaldav.root import RootResource
from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.directory import augment, calendaruserproxy
+from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
from twistedcaldav.directory.digest import QopDigestCredentialFactory
from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
from twistedcaldav.directory.aggregate import AggregateDirectoryService
from twistedcaldav.directory.sudo import SudoDirectoryService
from twistedcaldav.httpfactory import HTTP503LoggingFactory, LimitingHTTPFactory
from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import IScheduleInboxFile
from twistedcaldav.static import TimezoneServiceFile
from twistedcaldav.timezones import TimezoneCache
from twistedcaldav import pdmonster
@@ -502,6 +506,7 @@
rootResourceClass = RootResource
principalResourceClass = DirectoryPrincipalProvisioningResource
calendarResourceClass = CalendarHomeProvisioningFile
+ iScheduleResourceClass = IScheduleInboxFile
timezoneServiceResourceClass = TimezoneServiceFile
def makeService_Slave(self, options):
@@ -513,6 +518,32 @@
setLogLevelForNamespace(None, "info")
#
+ # Setup the Augment Service
+ #
+ augmentClass = namedClass(config.AugmentService.type)
+
+ log.info("Configuring augment service of type: %s" % (augmentClass,))
+
+ try:
+ augment.AugmentService = augmentClass(**config.AugmentService.params)
+ except IOError, e:
+ log.error("Could not start augment service")
+ raise
+
+ #
+ # Setup the ProxyDB Service
+ #
+ proxydbClass = namedClass(config.ProxyDBService.type)
+
+ log.info("Configuring proxydb service of type: %s" % (proxydbClass,))
+
+ try:
+ calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+ except IOError, e:
+ log.error("Could not start proxydb service")
+ raise
+
+ #
# Setup the Directory
#
directories = []
@@ -548,15 +579,22 @@
SudoDirectoryService.recordType_sudoers)
#
+ # Make sure proxies get initialized
+ #
+ if config.ProxyLoadFromFile:
+ def _doProxyUpdate():
+ loader = XMLCalendarUserProxyLoader(config.ProxyLoadFromFile)
+ return loader.updateProxyDB()
+
+ reactor.addSystemEventTrigger("after", "startup", _doProxyUpdate)
+
+ #
# Configure Memcached Client Pool
#
- if config.Memcached["ClientEnabled"]:
- memcachepool.installPool(
- IPv4Address(
- 'TCP',
- config.Memcached["BindAddress"],
- config.Memcached["Port"]),
- config.Memcached["MaxClients"])
+ memcachepool.installPools(
+ config.Memcached.Pools,
+ config.Memcached.MaxClients,
+ )
#
# Configure NotificationClient
@@ -593,6 +631,17 @@
root.putChild('principals', principalCollection)
root.putChild('calendars', calendarCollection)
+ # iSchedule service is optional
+ if config.Scheduling.iSchedule.Enabled:
+ log.info("Setting up iSchedule inbox resource: %r"
+ % (self.iScheduleResourceClass,))
+
+ ischedule = self.iScheduleResourceClass(
+ os.path.join(config.DocumentRoot, "ischedule"),
+ root,
+ )
+ root.putChild("ischedule", ischedule)
+
# Timezone service is optional
if config.EnableTimezoneService:
timezoneService = self.timezoneServiceResourceClass(
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_cache.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_cache.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_cache.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -18,7 +18,6 @@
import hashlib
import cPickle
-from twisted.trial.unittest import TestCase
from twisted.internet.defer import succeed, maybeDeferred
from twisted.web2.dav import davxml
@@ -31,6 +30,7 @@
from twistedcaldav.cache import PropfindCacheMixin
from twistedcaldav.test.util import InMemoryMemcacheProtocol
+from twistedcaldav.test.util import TestCase
def _newCacheToken(self):
@@ -87,6 +87,7 @@
class MemCacheChangeNotifierTests(TestCase):
def setUp(self):
+ TestCase.setUp(self)
self.memcache = InMemoryMemcacheProtocol()
self.ccn = MemcacheChangeNotifier(
StubURLResource(':memory:'),
@@ -336,7 +337,7 @@
self.tokens['/principals/__uids__/cdaboo/'] = 'principalToken0'
self.tokens['/principals/__uids__/dreid/'] = 'principalTokenX'
- def _getToken(uri):
+ def _getToken(uri, cachePoolHandle=None):
return succeed(self.tokens.get(uri))
self.rc._tokenForURI = _getToken
@@ -438,6 +439,7 @@
Test the PropfindCacheMixin
"""
def setUp(self):
+ TestCase.setUp(self)
self.resource = TestCachingResource(StubResponse(200, {}, "foobar"))
self.responseCache = StubResponseCacheResource()
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_config.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_config.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -158,7 +158,6 @@
self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.appleopendirectory.OpenDirectoryService")
self.assertNotIn("xmlFile", config.DirectoryService.params)
self.assertEquals(config.DirectoryService.params.node, "/Search")
- self.assertEquals(config.DirectoryService.params.requireComputerRecord, True)
self.assertEquals(config.DirectoryService.params.cacheTimeout, 30)
def testDirectoryService_newParam(self):
@@ -167,13 +166,11 @@
config.update({"DirectoryService": {"type": "twistedcaldav.directory.appleopendirectory.OpenDirectoryService"}})
config.update({"DirectoryService": {"params": {
- "requireComputerRecord": False,
"cacheTimeout": 12345,
}}})
self.assertEquals(config.DirectoryService.type, "twistedcaldav.directory.appleopendirectory.OpenDirectoryService")
self.assertEquals(config.DirectoryService.params.node, "/Search")
- self.assertEquals(config.DirectoryService.params.requireComputerRecord, False)
self.assertEquals(config.DirectoryService.params.cacheTimeout, 12345)
def testDirectoryService_unknownType(self):
Copied: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_database.py (from rev 4856, CalendarServer/branches/users/cdaboo/deployment-partition-4722/twistedcaldav/test/test_database.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_database.py (rev 0)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_database.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -0,0 +1,208 @@
+##
+# Copyright (c) 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.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin
+import twistedcaldav.test.util
+
+from twisted.internet.defer import inlineCallbacks
+
+import os
+import time
+
+class Database (twistedcaldav.test.util.TestCase):
+ """
+ Test abstract SQL DB class
+ """
+
+ class TestDB(ADBAPISqliteMixin, AbstractADBAPIDatabase):
+
+ def __init__(self, path, persistent=False, version="1"):
+ self.version = version
+ self.dbpath = path
+ super(Database.TestDB, self).__init__("sqlite", "sqlite3", (path,), persistent, cp_min=3, cp_max=3)
+
+ def _db_version(self):
+ """
+ @return: the schema version assigned to this index.
+ """
+ return self.version
+
+ def _db_type(self):
+ """
+ @return: the collection type assigned to this index.
+ """
+ return "TESTTYPE"
+
+ def _db_init_data_tables(self):
+ """
+ Initialise the underlying database tables.
+ @param q: a database cursor to use.
+ """
+
+ #
+ # TESTTYPE table
+ #
+ return self._db_execute(
+ """
+ create table TESTTYPE (
+ KEY text unique,
+ VALUE text
+ )
+ """
+ )
+
+ def _db_remove_data_tables(self):
+ return self._db_execute("drop table TESTTYPE")
+
+ class TestDBRecreateUpgrade(TestDB):
+
+ class RecreateDBException(Exception):
+ pass
+ class UpgradeDBException(Exception):
+ pass
+
+ def __init__(self, path, persistent=False):
+ super(Database.TestDBRecreateUpgrade, self).__init__(path, persistent, version="2")
+
+ def _db_recreate(self):
+ raise self.RecreateDBException()
+
+ class TestDBCreateIndexOnUpgrade(TestDB):
+
+ def __init__(self, path, persistent=False):
+ super(Database.TestDBCreateIndexOnUpgrade, self).__init__(path, persistent, version="2")
+
+ def _db_upgrade_data_tables(self, old_version):
+ return self._db_execute(
+ """
+ create index TESTING on TESTTYPE (VALUE)
+ """
+ )
+
+ class TestDBPauseInInit(TestDB):
+
+ def _db_init(self):
+
+ time.sleep(1)
+ super(Database.TestDBPauseInInit, self)._db_init()
+
+ @inlineCallbacks
+ def inlineCallbackRaises(self, exc, f, *args, **kwargs):
+ try:
+ yield f(*args, **kwargs)
+ except exc:
+ pass
+ except Exception, e:
+ self.fail("Wrong exception raised: %s" % (e,))
+ else:
+ self.fail("%s not raised" % (exc,))
+
+ @inlineCallbacks
+ def test_connect(self):
+ """
+ Connect to database and create table
+ """
+ db = Database.TestDB(self.mktemp())
+ self.assertFalse(db.initialized)
+ yield db.open()
+ self.assertTrue(db.initialized)
+
+ @inlineCallbacks
+ def test_readwrite(self):
+ """
+ Add a record, search for it
+ """
+ db = Database.TestDB(self.mktemp())
+ yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", ("FOO", "BAR",))
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, (("FOO", "BAR"),))
+ items = (yield db.queryList("SELECT * from TESTTYPE"))
+ self.assertEqual(items, ("FOO",))
+
+ @inlineCallbacks
+ def test_close(self):
+ """
+ Close database
+ """
+ db = Database.TestDB(self.mktemp())
+ self.assertFalse(db.initialized)
+ yield db.open()
+ db.close()
+ self.assertFalse(db.initialized)
+ db.close()
+
+ @inlineCallbacks
+ def test_version_upgrade_nonpersistent(self):
+ """
+ Connect to database and create table
+ """
+
+ db_file = self.mktemp()
+
+ db = Database.TestDB(db_file)
+ yield db.open()
+ yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", ("FOO", "BAR",))
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, (("FOO", "BAR"),))
+ db.close()
+ db = None
+
+ db = Database.TestDBRecreateUpgrade(db_file)
+ yield self.inlineCallbackRaises(Database.TestDBRecreateUpgrade.RecreateDBException, db.open)
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, ())
+
+ def test_version_upgrade_persistent(self):
+ """
+ Connect to database and create table
+ """
+ db_file = self.mktemp()
+ db = Database.TestDB(db_file, persistent=True)
+ yield db.open()
+ yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", "FOO", "BAR")
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, (("FOO", "BAR")))
+ db.close()
+ db = None
+
+ db = Database.TestDBRecreateUpgrade(db_file, persistent=True)
+ yield self.inlineCallbackRaises(NotImplementedError, db.open)
+ self.assertTrue(os.path.exists(db_file))
+ db.close()
+ db = None
+
+ db = Database.TestDB(db_file, persistent=True, autocommit=True)
+ yield db.open()
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, (("FOO", "BAR")))
+
+ def test_version_upgrade_persistent_add_index(self):
+ """
+ Connect to database and create table
+ """
+ db_file = self.mktemp()
+ db = Database.TestDB(db_file, persistent=True, autocommit=True)
+ yield db.open()
+ yield db.execute("INSERT into TESTTYPE (KEY, VALUE) values (:1, :2)", "FOO", "BAR")
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, (("FOO", "BAR")))
+ db.close()
+ db = None
+
+ db = Database.TestDBCreateIndexOnUpgrade(db_file, persistent=True, autocommit=True)
+ yield db.open()
+ items = (yield db.query("SELECT * from TESTTYPE"))
+ self.assertEqual(items, (("FOO", "BAR")))
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_log.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_log.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -17,7 +17,7 @@
from twistedcaldav.log import *
from twistedcaldav.log import logLevelsByNamespace
-import twisted.trial.unittest
+from twistedcaldav.test.util import TestCase
defaultLogLevel = logLevelsByNamespace[None]
@@ -37,7 +37,7 @@
class LoggingEnabledObject (LoggingMixIn):
pass
-class Logging (twisted.trial.unittest.TestCase):
+class Logging (TestCase):
def test_cmpLogLevels(self):
self.assertEquals(cmpLogLevels("info" , "error"), -1)
self.assertEquals(cmpLogLevels("debug", "debug"), 0)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcache.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcache.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcache.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -7,14 +7,12 @@
from twistedcaldav.memcache import MemCacheProtocol, NoSuchCommand
from twistedcaldav.memcache import ClientError, ServerError
+from twistedcaldav.test.util import TestCase
-from twisted.trial.unittest import TestCase
from twisted.test.proto_helpers import StringTransportWithDisconnection
from twisted.internet.task import Clock
from twisted.internet.defer import Deferred, gatherResults, TimeoutError
-
-
class MemCacheTestCase(TestCase):
"""
Test client protocol class L{MemCacheProtocol}.
@@ -25,6 +23,7 @@
Create a memcache client, connect it to a string protocol, and make it
use a deterministic clock.
"""
+ TestCase.setUp(self)
self.proto = MemCacheProtocol()
self.clock = Clock()
self.proto.callLater = self.clock.callLater
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcachepool.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcachepool.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcachepool.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -16,8 +16,6 @@
from zope.interface import implements
-from twisted.trial.unittest import TestCase
-
from twisted.internet.interfaces import IConnector, IReactorTCP
from twisted.internet.address import IPv4Address
@@ -25,8 +23,8 @@
from twistedcaldav.memcachepool import PooledMemCacheProtocol
from twistedcaldav.memcachepool import MemCacheClientFactory
from twistedcaldav.memcachepool import MemCachePool
+from twistedcaldav.test.util import TestCase
-
MC_ADDRESS = IPv4Address('TCP', '127.0.0.1', 11211)
@@ -137,6 +135,7 @@
Create a L{MemCacheClientFactory} instance and and give it a
L{StubConnectionPool} instance.
"""
+ TestCase.setUp(self)
self.pool = StubConnectionPool()
self.factory = MemCacheClientFactory()
self.factory.connectionPool = self.pool
@@ -196,6 +195,7 @@
"""
Create a L{MemCachePool}.
"""
+ TestCase.setUp(self)
self.reactor = StubReactor()
self.pool = MemCachePool(MC_ADDRESS,
maxClients=5,
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcacher.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcacher.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_memcacher.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -6,10 +6,10 @@
"""
from twisted.internet.defer import inlineCallbacks
-from twistedcaldav.test.util import TestCase
from twistedcaldav.config import config
from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.test.util import TestCase
class MemcacherTestCase(TestCase):
"""
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_resource.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_resource.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -14,13 +14,11 @@
# limitations under the License.
##
-from twistedcaldav.test.util import TestCase
-
from twistedcaldav.resource import CalDAVResource
from twistedcaldav.test.util import InMemoryPropertyStore
+from twistedcaldav.test.util import TestCase
-
class StubProperty(object):
def qname(self):
return "StubQname"
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_root.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_root.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_root.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -18,9 +18,10 @@
from twistedcaldav.root import RootResource
from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory import augment
from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile
+from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
from twisted.cred.portal import Portal
@@ -60,6 +61,7 @@
'calendar': ['dreid']})
directory = XMLDirectoryService(xmlFile)
+ augment.AugmentService = augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
principals = DirectoryPrincipalProvisioningResource('/principals/', directory)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_static.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_static.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -14,11 +14,10 @@
# limitations under the License.
##
-from twistedcaldav.test.util import TestCase
-
from twistedcaldav.static import CalendarHomeFile, CalDAVFile
from twistedcaldav.cache import DisabledCacheNotifier
from twistedcaldav.test.util import StubCacheChangeNotifier
+from twistedcaldav.test.util import TestCase
class StubParentResource(object):
def principalCollections(self):
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_tap.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/test_tap.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -17,9 +17,6 @@
import os
from copy import deepcopy
-from twisted.trial import unittest
-from twistedcaldav.test.util import TestCase
-
from twisted.python.usage import Options, UsageError
from twisted.python.util import sibpath
from twisted.python.reflect import namedAny
@@ -40,6 +37,7 @@
from twistedcaldav.directory.sudo import SudoDirectoryService
from twistedcaldav.directory.directory import UnknownRecordTypeError
+from twistedcaldav.test.util import TestCase
class TestCalDAVOptions(CalDAVOptions):
"""
@@ -184,12 +182,19 @@
accountsFile = sibpath(os.path.dirname(__file__),
'directory/test/accounts.xml')
+ augmentsFile = sibpath(os.path.dirname(__file__),
+ 'directory/test/augments.xml')
self.config['DirectoryService'] = {
'params': {'xmlFile': accountsFile},
'type': 'twistedcaldav.directory.xmlfile.XMLDirectoryService'
}
+ self.config["AugmentService"] = {
+ "params": {"xmlFiles": [augmentsFile]},
+ "type": "twistedcaldav.directory.augment.AugmentXMLDB"
+ }
+
self.config['DocumentRoot'] = self.mktemp()
self.config['DataRoot'] = self.mktemp()
self.config['ProcessType'] = 'Slave'
@@ -198,6 +203,9 @@
self.config['SudoersFile'] = ''
+ self.config['Memcached']['Pools']['Default']['ClientEnabled'] = False
+ self.config['Memcached']['Pools']['Default']['ServerEnabled'] = False
+
if self.configOptions:
config_mod._mergeData(self.config, self.configOptions)
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/util.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/test/util.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -41,8 +41,8 @@
dataroot = self.mktemp()
os.mkdir(dataroot)
config.DataRoot = dataroot
- config.Memcached.ClientEnabled = False
- config.Memcached.ServerEnabled = False
+ config.Memcached.Pools.Default.ClientEnabled = False
+ config.Memcached.Pools.Default.ServerEnabled = False
memcacheclient.ClientFactory.allowTestCache = True
def createHierarchy(self, structure):
Modified: CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/util.py 2009-12-11 17:20:46 UTC (rev 4856)
+++ CalendarServer/branches/users/cdaboo/deployment-partition-4855/twistedcaldav/util.py 2009-12-11 18:41:21 UTC (rev 4857)
@@ -108,3 +108,8 @@
state = self._state
self._state = not state
return state
+
+def utf8String(s):
+ if isinstance(s, unicode):
+ s = s.encode("utf-8")
+ return s
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20091211/d10e92ef/attachment-0001.html>
More information about the calendarserver-changes
mailing list