[CalendarServer-changes] [13158] CalendarServer/branches/users/sagen/move2who-5

source_changes at macosforge.org source_changes at macosforge.org
Fri Apr 4 10:20:27 PDT 2014


Revision: 13158
          http://trac.calendarserver.org//changeset/13158
Author:   sagen at apple.com
Date:     2014-04-04 10:20:27 -0700 (Fri, 04 Apr 2014)
Log Message:
-----------
pull up from trunk

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/accesslog.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/root.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/test/test_root.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/push/applepush.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/caldav.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_caldav.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_util.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/util.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/agent.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/calverify.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/export.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/gateway.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/migrate.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/principals.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/purge.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/push.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/resources.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/directory.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/terminal.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/test/test_vfs.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/vfs.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/augments.xml
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/caldavd.plist
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/resources-locations.xml
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/users-groups.xml
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/caldavd.plist
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/resources-locations.xml
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/users-groups.xml
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_agent.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_calverify.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_gateway.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_principals.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge_old_events.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_resources.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/util.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/webadmin/principals.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/webcal/resource.py
    CalendarServer/branches/users/sagen/move2who-5/conf/auth/accounts-test.xml
    CalendarServer/branches/users/sagen/move2who-5/conf/auth/augments-test.xml
    CalendarServer/branches/users/sagen/move2who-5/conf/auth/proxies-test.xml
    CalendarServer/branches/users/sagen/move2who-5/conf/auth/resources-test.xml
    CalendarServer/branches/users/sagen/move2who-5/conf/caldavd-test.plist
    CalendarServer/branches/users/sagen/move2who-5/contrib/performance/loadtest/test_sim.py
    CalendarServer/branches/users/sagen/move2who-5/requirements/py_develop.txt
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/cache.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/customxml.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/addressbook.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/augment.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendar.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxyloader.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/common.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory-principal-resource.html
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/principal.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/accounts.xml
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/augments.xml
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/resources.xml
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_augment.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_principal.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/util.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/util.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directorybackedaddressbook.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/extensions.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/freebusyurl.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/ical.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_addressbook_query.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_common.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_multiget_common.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/resource.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/scheduling_store/caldav/resource.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/sharing.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/storebridge.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookmultiget.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookquery.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_cache.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_calendarquery.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_collectioncontents.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_config.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_icalendar.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_mkcalendar.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_multiget.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_props.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_resource.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_sharing.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_upgrade.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_wrapping.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/util.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezoneservice.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezonestdservice.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/upgrade.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/util.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/caldav/scheduler.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/freebusy.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/imip/inbound.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/delivery.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/resource.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/itip.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/scheduler.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/work.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/accounts.xml
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/resources.xml
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/test_attachments.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/util.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/util.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/icalendardirectoryservice.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/carddav/datastore/query/filter.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/file.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/conduit.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/resource.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/test/test_conduit.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/sql.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/test/util.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/dps/client.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/dps/commands.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/dps/server.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test.xml
    CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test_client.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/delegates.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/groups.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/idirectory.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/accounts.xml
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/resources.xml
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_delegates.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_groups.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/xml.py
    CalendarServer/branches/users/sagen/move2who-5/txweb2/channel/http.py
    CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/method/report_expand.py
    CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/resource.py
    CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/util.py
    CalendarServer/branches/users/sagen/move2who-5/txweb2/server.py

Added Paths:
-----------
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/platform/darwin/od/
    CalendarServer/branches/users/sagen/move2who-5/conf/auth/generate_test_accounts.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/dps/json.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/augment.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/directory.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/augments.xml
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_augment.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_directory.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_util.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_wiki.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/util.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/vcard.py
    CalendarServer/branches/users/sagen/move2who-5/txdav/who/wiki.py

Removed Paths:
-------------
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/platform/darwin/od/
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/platform/darwin/wiki.py
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/calverify/
    CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/purge/
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/aggregate.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/appleopendirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/cachingdirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/idirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/ldapdirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/opendirectorybacker.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_aggregate.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_buildquery.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_cachedirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_directory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_guidchange.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_ldapdirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_livedirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_modify.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectory.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectorybacker.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_proxyprincipalmembers.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_resources.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_wiki.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_xmlfile.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/wiki.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlaccountsparser.py
    CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlfile.py

Property Changed:
----------------
    CalendarServer/branches/users/sagen/move2who-5/


Property changes on: CalendarServer/branches/users/sagen/move2who-5
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalDAVTester/trunk:11193-11198
/CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/release/CalendarServer-5.2-dev:11972,12357-12358,12794,12814
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/cross-pod-sharing:12038-12191
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/performance-tweaks:11824-11836
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/scheduling-queue-refresh:11783-12557
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/cleanrevisions:12152-12334
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/start-service-start-loop:11060-11065
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593
   + /CalDAVTester/trunk:11193-11198
/CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/release/CalendarServer-5.1-dev:11846
/CalendarServer/branches/release/CalendarServer-5.2-dev:11972,12357-12358,12794,12814
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/cross-pod-sharing:12038-12191
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/fix-no-ischedule:11607-11871
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/json:11622-11912
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/performance-tweaks:11824-11836
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/reverse-proxy-pods:11875-11900
/CalendarServer/branches/users/cdaboo/scheduling-queue-refresh:11783-12557
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/sharing-in-the-store:11935-12016
/CalendarServer/branches/users/cdaboo/store-scheduling:10876-11129
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/gaya/cleanrevisions:12152-12334
/CalendarServer/branches/users/gaya/sharedgroupfixes:12120-12142
/CalendarServer/branches/users/gaya/sharedgroups-3:11088-11204
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/enforce-max-requests:11640-11643
/CalendarServer/branches/users/glyph/hang-fix:11465-11491
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/launchd-wrapper-bis:11413-11436
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/log-cleanups:11691-11731
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/start-service-start-loop:11060-11065
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/warning-cleanups:11347-11357
/CalendarServer/branches/users/glyph/whenNotProposed:11881-11897
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/move2who:12819-12860
/CalendarServer/branches/users/sagen/move2who-2:12861-12898
/CalendarServer/branches/users/sagen/move2who-3:12899-12913
/CalendarServer/branches/users/sagen/move2who-4:12914-13157
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/sagen/testing:10827-10851,10853-10855
/CalendarServer/branches/users/wsanchez/transations:5515-5593

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/accesslog.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/accesslog.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/accesslog.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -48,7 +48,6 @@
 from twisted.protocols import amp
 
 from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryService
 
 from txdav.xml import element as davxml
 
@@ -91,22 +90,27 @@
                     if hasattr(request, "authzUser") and str(request.authzUser.children[0]) != uidn:
                         uidz = str(request.authzUser.children[0])
 
-                    def convertUIDtoShortName(uid):
-                        uid = uid.rstrip("/")
-                        uid = uid[uid.rfind("/") + 1:]
-                        record = request.site.resource.getDirectory().recordWithUID(uid)
-                        if record:
-                            if record.recordType == DirectoryService.recordType_users:
-                                return record.shortNames[0]
-                            else:
-                                return "(%s)%s" % (record.recordType, record.shortNames[0],)
-                        else:
-                            return uid
+                    # def convertUIDtoShortName(uid):
+                    #     uid = uid.rstrip("/")
+                    #     uid = uid[uid.rfind("/") + 1:]
+                    #     record = request.site.resource.getDirectory().recordWithUID(uid)
+                    #     if record:
+                    #         if record.recordType == DirectoryService.recordType_users:
+                    #             return record.shortNames[0]
+                    #         else:
+                    #             return "(%s)%s" % (record.recordType, record.shortNames[0],)
+                    #     else:
+                    #         return uid
 
-                    uidn = convertUIDtoShortName(uidn)
-                    if uidz:
-                        uidz = convertUIDtoShortName(uidz)
+                    # MOVE2WHO
+                    # Better to stick the records directly on the request at
+                    # an earlier point, since we can't do anything deferred
+                    # in here.
 
+                    # uidn = convertUIDtoShortName(uidn)
+                    # if uidz:
+                    #     uidz = convertUIDtoShortName(uidz)
+
                     if uidn and uidz:
                         uid = '"%s as %s"' % (uidn, uidz,)
                     else:
@@ -151,8 +155,9 @@
                 format += ' i=%(serverInstance)s'
                 formatArgs["serverInstance"] = config.LogID if config.LogID else "0"
 
-                format += ' or=%(outstandingRequests)s'
-                formatArgs["outstandingRequests"] = request.chanRequest.channel.factory.outstandingRequests
+                if request.chanRequest:  # This can be None during tests
+                    format += ' or=%(outstandingRequests)s'
+                    formatArgs["outstandingRequests"] = request.chanRequest.channel.factory.outstandingRequests
 
                 # Tags for time stamps collected along the way - the first one in the list is the initial
                 # time for request creation - we use that to track the entire request/response time

Deleted: CalendarServer/branches/users/sagen/move2who-5/calendarserver/platform/darwin/wiki.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/platform/darwin/wiki.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/platform/darwin/wiki.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,102 +0,0 @@
-##
-# Copyright (c) 2012-2014 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 twext.python.log import Logger
-from twext.internet.gaiendpoint import GAIEndpoint
-from twext.internet.adaptendpoint import connect
-
-from twisted.web.client import HTTPPageGetter, HTTPClientFactory
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-import json
-
-log = Logger()
-
- at inlineCallbacks
-def guidForAuthToken(token, host="localhost", port=80):
-    """
-    Send a GET request to the web auth service to retrieve the user record
-    guid associated with the provided auth token.
-
-    @param token: An auth token, usually passed in via cookie when webcal
-        makes a request.
-    @type token: C{str}
-    @return: deferred returning a guid (C{str}) if successful, or
-        will raise WebAuthError otherwise.
-    """
-    url = "http://%s:%d/auth/verify?auth_token=%s" % (host, port, token,)
-    jsonResponse = (yield _getPage(url, host, port))
-    try:
-        response = json.loads(jsonResponse)
-    except Exception, e:
-        log.error("Error parsing JSON response from webauth: %s (%s)" %
-            (jsonResponse, str(e)))
-        raise WebAuthError("Could not look up token: %s" % (token,))
-    if response["succeeded"]:
-        returnValue(response["generated_uid"])
-    else:
-        raise WebAuthError("Could not look up token: %s" % (token,))
-
-
-
-def accessForUserToWiki(user, wiki, host="localhost", port=4444):
-    """
-    Send a GET request to the wiki collabd service to retrieve the access level
-    the given user (in GUID form) has to the given wiki (in wiki short-name
-    form).
-
-    @param user: The GUID of the user
-    @type user: C{str}
-    @param wiki: The short name of the wiki
-    @type wiki: C{str}
-    @return: deferred returning a access level (C{str}) if successful, or
-        if the user is not recognized a twisted.web.error.Error with
-        status FORBIDDEN will errBack; an unknown wiki will have a status
-        of NOT_FOUND
-    """
-    url = "http://%s:%s/cal/accessLevelForUserWikiCalendar/%s/%s" % (host, port,
-        user, wiki)
-    return _getPage(url, host, port)
-
-
-
-def _getPage(url, host, port):
-    """
-    Fetch the body of the given url via HTTP, connecting to the given host
-    and port.
-
-    @param url: The URL to GET
-    @type url: C{str}
-    @param host: The hostname to connect to
-    @type host: C{str}
-    @param port: The port number to connect to
-    @type port: C{int}
-    @return: A deferred; upon 200 success the body of the response is returned,
-        otherwise a twisted.web.error.Error is the result.
-    """
-    factory = HTTPClientFactory(url)
-    factory.protocol = HTTPPageGetter
-    connect(GAIEndpoint(reactor, host, port), factory)
-    return factory.deferred
-
-
-
-class WebAuthError(RuntimeError):
-    """
-    Error in web auth
-    """

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/root.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/root.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/root.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,33 +20,34 @@
 ]
 
 from twext.python.log import Logger
-from txweb2 import responsecode
-from txweb2.auth.wrapper import UnauthorizedResponse
-from txdav.xml import element as davxml
-from txweb2.dav.xattrprops import xattrPropertyStore
-from txweb2.http import HTTPError, StatusResponse, RedirectResponse
-
 from twisted.cred.error import LoginFailed, UnauthorizedLogin
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twisted.python.reflect import namedClass
-from twisted.web.xmlrpc import Proxy
 from twisted.web.error import Error as WebError
-
+from twistedcaldav.cache import DisabledCache
+from twistedcaldav.cache import MemcacheResponseCache, MemcacheChangeNotifier
 from twistedcaldav.cache import _CachedResponseResource
-from twistedcaldav.cache import MemcacheResponseCache, MemcacheChangeNotifier
-from twistedcaldav.cache import DisabledCache
 from twistedcaldav.config import config
+from twistedcaldav.directory.principal import DirectoryPrincipalResource
 from twistedcaldav.extensions import DAVFile, CachingPropertyStore
 from twistedcaldav.extensions import DirectoryPrincipalPropertySearchMixIn
 from twistedcaldav.extensions import ReadOnlyResourceMixIn
 from twistedcaldav.resource import CalDAVComplianceMixIn
-from twistedcaldav.directory.principal import DirectoryPrincipalResource
-from calendarserver.platform.darwin.wiki import guidForAuthToken
+from txdav.who.wiki import DirectoryService as WikiDirectoryService
+from txdav.who.wiki import uidForAuthToken
+from txdav.xml import element as davxml
+from txweb2 import responsecode
+from txweb2.auth.wrapper import UnauthorizedResponse
+from txweb2.dav.xattrprops import xattrPropertyStore
+from txweb2.http import HTTPError, StatusResponse, RedirectResponse
 
 log = Logger()
 
 
-class RootResource (ReadOnlyResourceMixIn, DirectoryPrincipalPropertySearchMixIn, CalDAVComplianceMixIn, DAVFile):
+class RootResource(
+    ReadOnlyResourceMixIn, DirectoryPrincipalPropertySearchMixIn,
+    CalDAVComplianceMixIn, DAVFile
+):
     """
     A special root resource that contains support checking SACLs
     as well as adding responseFilters.
@@ -58,17 +59,17 @@
     # starts with any of these, then the list of SACLs are checked.  If the
     # request path does not start with any of these, then no SACLs are checked.
     saclMap = {
-        "addressbooks" : ("addressbook",),
-        "calendars" : ("calendar",),
-        "directory" : ("addressbook",),
-        "principals" : ("addressbook", "calendar"),
-        "webcal" : ("calendar",),
+        "addressbooks": ("addressbook",),
+        "calendars": ("calendar",),
+        "directory": ("addressbook",),
+        "principals": ("addressbook", "calendar"),
+        "webcal": ("calendar",),
     }
 
     # If a top-level resource path starts with any of these, an unauthenticated
     # request is redirected to the auth url (config.WebCalendarAuthPath)
     authServiceMap = {
-        "webcal" : True,
+        "webcal": True,
     }
 
     def __init__(self, path, *args, **kwargs):
@@ -82,11 +83,17 @@
 
         self.contentFilters = []
 
-        if config.EnableResponseCache and config.Memcached.Pools.Default.ClientEnabled:
+        if (
+            config.EnableResponseCache and
+            config.Memcached.Pools.Default.ClientEnabled
+        ):
             self.responseCache = MemcacheResponseCache(self.fp)
 
-            # These class attributes need to be setup with our memcache notifier
-            DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier
+            # These class attributes need to be setup with our memcache\
+            # notifier
+            DirectoryPrincipalResource.cacheNotifierFactory = (
+                MemcacheChangeNotifier
+            )
         else:
             self.responseCache = DisabledCache()
 
@@ -98,7 +105,9 @@
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):
             # Get the property store from super
-            deadProperties = namedClass(config.RootResourcePropStoreClass)(self)
+            deadProperties = (
+                namedClass(config.RootResourcePropStoreClass)(self)
+            )
 
             # Wrap the property store in a memory store
             if isinstance(deadProperties, xattrPropertyStore):
@@ -110,7 +119,7 @@
 
 
     def defaultAccessControlList(self):
-        return config.RootResourceACL
+        return succeed(config.RootResourceACL)
 
 
     @inlineCallbacks
@@ -160,7 +169,9 @@
         request.checkingSACL = True
 
         for collection in self.principalCollections():
-            principal = collection._principalForURI(authzUser.children[0].children[0].data)
+            principal = yield collection._principalForURI(
+                authzUser.children[0].children[0].data
+            )
             if principal is None:
                 response = (yield UnauthorizedResponse.makeResponse(
                     request.credentialFactories,
@@ -185,7 +196,10 @@
         if access:
             returnValue(True)
 
-        log.warn("User %r is not enabled with the %r SACL(s)" % (username, saclServices,))
+        log.warn(
+            "User {user!r} is not enabled with the {sacl!r} SACL(s)",
+            user=username, sacl=saclServices
+        )
         raise HTTPError(responsecode.FORBIDDEN)
 
 
@@ -229,54 +243,71 @@
                     token = None
 
                 if token is not None and token != "unauthenticated":
-                    log.debug("Wiki sessionID cookie value: %s" % (token,))
+                    log.debug(
+                        "Wiki sessionID cookie value: {token}", token=token
+                    )
 
                     record = None
                     try:
-                        if wikiConfig.LionCompatibility:
-                            guid = None
-                            proxy = Proxy(wikiConfig["URL"])
-                            username = (yield proxy.callRemote(wikiConfig["UserMethod"], token))
-                            directory = request.site.resource.getDirectory()
-                            record = directory.recordWithShortName("users", username)
-                            if record is not None:
-                                guid = record.guid
-                        else:
-                            guid = (yield guidForAuthToken(token))
-                            if guid == "unauthenticated":
-                                guid = None
+                        uid = yield uidForAuthToken(token)
+                        if uid == "unauthenticated":
+                            uid = None
 
-                    except WebError, w:
-                        guid = None
+                    except WebError as w:
+                        uid = None
                         # FORBIDDEN status means it's an unknown token
                         if int(w.status) == responsecode.NOT_FOUND:
-                            log.debug("Unknown wiki token: %s" % (token,))
+                            log.debug(
+                                "Unknown wiki token: {token}", token=token
+                            )
                         else:
-                            log.error("Failed to look up wiki token %s: %s" %
-                                (token, w.message,))
+                            log.error(
+                                "Failed to look up wiki token {token}: "
+                                "{message}",
+                                token=token, message=w.message
+                            )
 
-                    except Exception, e:
-                        log.error("Failed to look up wiki token (%s)" % (e,))
-                        guid = None
+                    except Exception as e:
+                        log.error(
+                            "Failed to look up wiki token: {error}",
+                            error=e
+                        )
+                        uid = None
 
-                    if guid is not None:
-                        log.debug("Wiki lookup returned guid: %s" % (guid,))
+                    if uid is not None:
+                        log.debug(
+                            "Wiki lookup returned uid: {uid}", uid=uid
+                        )
                         principal = None
                         directory = request.site.resource.getDirectory()
-                        record = directory.recordWithGUID(guid)
+                        record = yield directory.recordWithUID(uid)
                         if record is not None:
                             username = record.shortNames[0]
-                            log.debug("Wiki user record for user %s : %s" % (username, record))
+                            log.debug(
+                                "Wiki user record for user {user}: {record}",
+                                user=username, record=record
+                            )
                             for collection in self.principalCollections():
-                                principal = collection.principalForRecord(record)
+                                principal = (
+                                    yield collection.principalForRecord(record)
+                                )
                                 if principal is not None:
                                     break
 
                         if principal:
-                            log.debug("Wiki-authenticated principal %s being assigned to authnUser and authzUser" % (record.uid,))
-                            request.authzUser = request.authnUser = davxml.Principal(
-                                davxml.HRef.fromString("/principals/__uids__/%s/" % (record.uid,))
+                            log.debug(
+                                "Wiki-authenticated principal {record.uid} "
+                                "being assigned to authnUser and authzUser",
+                                record=record
                             )
+                            request.authzUser = request.authnUser = (
+                                davxml.Principal(
+                                    davxml.HRef.fromString(
+                                        "/principals/__uids__/{}/"
+                                        .format(record.uid)
+                                    )
+                                )
+                            )
 
         if not hasattr(request, "authzUser") and config.WebCalendarAuthPath:
             topLevel = request.path.strip("/").split("/")[0]
@@ -286,25 +317,27 @@
 
                 # Use config.ServerHostName if no x-forwarded-host header,
                 # otherwise use the final hostname in x-forwarded-host.
-                host = request.headers.getRawHeaders("x-forwarded-host",
-                    [config.ServerHostName])[-1].split(",")[-1].strip()
+                host = request.headers.getRawHeaders(
+                    "x-forwarded-host",
+                    [config.ServerHostName]
+                )[-1].split(",")[-1].strip()
                 port = 443 if config.EnableSSL else 80
                 scheme = "https" if config.EnableSSL else "http"
 
                 response = RedirectResponse(
-                        request.unparseURL(
-                            host=host,
-                            port=port,
-                            scheme=scheme,
-                            path=config.WebCalendarAuthPath,
-                            querystring="redirect=%s://%s%s" % (
-                                scheme,
-                                host,
-                                request.path
-                            )
-                        ),
-                        temporary=True
-                    )
+                    request.unparseURL(
+                        host=host,
+                        port=port,
+                        scheme=scheme,
+                        path=config.WebCalendarAuthPath,
+                        querystring="redirect={}://{}{}".format(
+                            scheme,
+                            host,
+                            request.path
+                        )
+                    ),
+                    temporary=True
+                )
                 raise HTTPError(response)
 
         # We don't want the /inbox resource to pay attention to SACLs because
@@ -314,10 +347,17 @@
         if segments[0] in ("inbox", "timezones"):
             request.checkedSACL = True
 
-        elif (len(segments) > 2 and segments[0] in ("calendars", "principals") and
+        elif (
             (
-                segments[1] == "wikis" or
-                (segments[1] == "__uids__" and segments[2].startswith("wiki-"))
+                len(segments) > 2 and
+                segments[0] in ("calendars", "principals") and
+                (
+                    segments[1] == "wikis" or
+                    (
+                        segments[1] == "__uids__" and
+                        segments[2].startswith(WikiDirectoryService.uidPrefix)
+                    )
+                )
             )
         ):
             # This is a wiki-related calendar resource. SACLs are not checked.
@@ -332,12 +372,21 @@
                 else:
                     wikiName = segments[2][5:]
                 if wikiName:
-                    log.debug("Wiki principal %s being assigned to authzUser" % (wikiName,))
+                    log.debug(
+                        "Wiki principal {name} being assigned to authzUser",
+                        name=wikiName
+                    )
                     request.authzUser = davxml.Principal(
-                        davxml.HRef.fromString("/principals/wikis/%s/" % (wikiName,))
+                        davxml.HRef.fromString(
+                            "/principals/wikis/{}/".format(wikiName)
+                        )
                     )
 
-        elif self.useSacls and not hasattr(request, "checkedSACL") and not hasattr(request, "checkingSACL"):
+        elif (
+            self.useSacls and
+            not hasattr(request, "checkedSACL") and
+            not hasattr(request, "checkingSACL")
+        ):
             yield self.checkSacl(request)
 
         if config.RejectClients:
@@ -348,28 +397,37 @@
             if agent is not None:
                 for reject in config.RejectClients:
                     if reject.search(agent) is not None:
-                        log.info("Rejecting user-agent: %s" % (agent,))
+                        log.info("Rejecting user-agent: {agent}", agent=agent)
                         raise HTTPError(StatusResponse(
                             responsecode.FORBIDDEN,
-                            "Your client software (%s) is not allowed to access this service." % (agent,)
+                            "Your client software ({}) is not allowed to "
+                            "access this service."
+                            .format(agent)
                         ))
 
-        if config.EnableResponseCache and request.method == "PROPFIND" and not getattr(request, "notInCache", False) and len(segments) > 1:
+        if (
+            config.EnableResponseCache and
+            request.method == "PROPFIND" and
+            not getattr(request, "notInCache", False) and
+            len(segments) > 1
+        ):
             try:
-                authnUser, authzUser = (yield self.authenticate(request))
+                authnUser, authzUser = yield self.authenticate(request)
                 request.authnUser = authnUser
                 request.authzUser = authzUser
             except (UnauthorizedLogin, LoginFailed):
-                response = (yield UnauthorizedResponse.makeResponse(
+                response = yield UnauthorizedResponse.makeResponse(
                     request.credentialFactories,
                     request.remoteAddr
-                ))
+                )
                 raise HTTPError(response)
 
             try:
                 if not getattr(request, "checkingCache", False):
                     request.checkingCache = True
-                    response = (yield self.responseCache.getResponseForRequest(request))
+                    response = yield self.responseCache.getResponseForRequest(
+                        request
+                    )
                     if response is None:
                         request.notInCache = True
                         raise KeyError("Not found in cache.")
@@ -378,7 +436,9 @@
             except KeyError:
                 pass
 
-        child = (yield super(RootResource, self).locateChild(request, segments))
+        child = yield super(RootResource, self).locateChild(
+            request, segments
+        )
         returnValue(child)
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/test/test_root.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/test/test_root.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/provision/test/test_root.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -14,29 +14,22 @@
 # limitations under the License.
 ##
 
-import os
 
-from twisted.cred.portal import Portal
 from twisted.internet.defer import inlineCallbacks, maybeDeferred, returnValue
 
+from twext.who.idirectory import RecordType
 from txweb2 import http_headers
 from txweb2 import responsecode
-from txweb2 import server
-from txweb2.auth import basic
-from txweb2.dav import auth
 from txdav.xml import element as davxml
 from txweb2.http import HTTPError
 from txweb2.iweb import IResponse
-from txweb2.test.test_server import SimpleRequest
 
-from twistedcaldav.test.util import TestCase
+from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
 
 from calendarserver.provision.root import RootResource
-from twistedcaldav.directory import augment
 
+
 class FakeCheckSACL(object):
     def __init__(self, sacls=None):
         self.sacls = sacls or {}
@@ -53,48 +46,16 @@
 
 
 
-class RootTests(TestCase):
+class RootTests(StoreTestCase):
 
+    @inlineCallbacks
     def setUp(self):
-        super(RootTests, self).setUp()
+        yield super(RootTests, self).setUp()
 
-        self.docroot = self.mktemp()
-        os.mkdir(self.docroot)
-
         RootResource.CheckSACL = FakeCheckSACL(sacls={"calendar": ["dreid"]})
 
-        directory = XMLDirectoryService(
-            {
-                "xmlFile" : xmlFile,
-                "augmentService" :
-                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,))
-            }
-        )
 
-        principals = DirectoryPrincipalProvisioningResource(
-            "/principals/",
-            directory
-        )
 
-        root = RootResource(self.docroot, principalCollections=[principals])
-
-        root.putChild("principals",
-                      principals)
-
-        portal = Portal(auth.DavRealm())
-        portal.registerChecker(directory)
-
-        self.root = auth.AuthenticationWrapper(
-            root,
-            portal,
-            (basic.BasicCredentialFactory("Test realm"),),
-            (basic.BasicCredentialFactory("Test realm"),),
-            loginInterfaces=(auth.IPrincipal,))
-
-        self.site = server.Site(self.root)
-
-
-
 class ComplianceTests(RootTests):
     """
     Tests to verify CalDAV compliance of the root resource.
@@ -107,8 +68,8 @@
         Deferred which will fire with (something adaptable to) an HTTP response
         object.
         """
-        request = SimpleRequest(self.site, method, ("/".join([""] + segments)))
-        rsrc = self.root
+        request = SimpleStoreRequest(self, method, ("/".join([""] + segments)))
+        rsrc = self.actualRoot
         while segments:
             rsrc, segments = (yield maybeDeferred(
                 rsrc.locateChild, request, segments
@@ -138,18 +99,12 @@
 
         should return a valid resource
         """
-        self.root.resource.useSacls = False
+        self.actualRoot.useSacls = False
 
-        request = SimpleRequest(self.site,
-                                "GET",
-                                "/principals/")
+        request = SimpleStoreRequest(self, "GET", "/principals/")
 
-        resrc, _ignore_segments = (yield maybeDeferred(
-            self.root.locateChild, request, ["principals"]
-        ))
-
         resrc, segments = (yield maybeDeferred(
-            resrc.locateChild, request, ["principals"]
+            self.actualRoot.locateChild, request, ["principals"]
         ))
 
         self.failUnless(
@@ -169,26 +124,21 @@
 
         should return a valid resource
         """
-        self.root.resource.useSacls = True
+        self.actualRoot.useSacls = True
 
-        request = SimpleRequest(
-            self.site,
+        record = yield self.directory.recordWithShortName(
+            RecordType.user,
+            u"dreid"
+        )
+        request = SimpleStoreRequest(
+            self,
             "GET",
             "/principals/",
-            headers=http_headers.Headers({
-                "Authorization": [
-                    "basic",
-                    "%s" % ("dreid:dierd".encode("base64"),)
-                ]
-            })
+            authRecord=record
         )
 
-        resrc, _ignore_segments = (yield maybeDeferred(
-            self.root.locateChild, request, ["principals"]
-        ))
-
         resrc, segments = (yield maybeDeferred(
-            resrc.locateChild, request, ["principals"]
+            self.actualRoot.locateChild, request, ["principals"]
         ))
 
         self.failUnless(
@@ -218,28 +168,27 @@
 
         should return a 403 forbidden response
         """
-        self.root.resource.useSacls = True
+        self.actualRoot.useSacls = True
 
-        request = SimpleRequest(
-            self.site,
+        record = yield self.directory.recordWithShortName(
+            RecordType.user,
+            u"wsanchez"
+        )
+
+        request = SimpleStoreRequest(
+            self,
             "GET",
             "/principals/",
-            headers=http_headers.Headers({
-                "Authorization": [
-                    "basic",
-                    "%s" % ("wsanchez:zehcnasw".encode("base64"),)
-                ]
-            })
+            authRecord=record
         )
 
-        resrc, _ignore_segments = (yield maybeDeferred(
-            self.root.locateChild, request, ["principals"]
-        ))
-
         try:
             resrc, _ignore_segments = (yield maybeDeferred(
-                resrc.locateChild, request, ["principals"]
+                self.actualRoot.locateChild, request, ["principals"]
             ))
+            raise AssertionError(
+                "RootResource.locateChild did not return an error"
+            )
         except HTTPError, e:
             self.assertEquals(e.response.code, 403)
 
@@ -253,20 +202,16 @@
         should return a 401 UnauthorizedResponse
         """
 
-        self.root.resource.useSacls = True
-        request = SimpleRequest(
-            self.site,
+        self.actualRoot.useSacls = True
+        request = SimpleStoreRequest(
+            self,
             "GET",
             "/principals/"
         )
 
-        resrc, _ignore_segments = (yield maybeDeferred(
-            self.root.locateChild, request, ["principals"]
-        ))
-
         try:
             resrc, _ignore_segments = (yield maybeDeferred(
-                resrc.locateChild, request, ["principals"]
+                self.actualRoot.locateChild, request, ["principals"]
             ))
             raise AssertionError(
                 "RootResource.locateChild did not return an error"
@@ -283,24 +228,28 @@
 
         should return a 401 UnauthorizedResponse
         """
-        self.root.resource.useSacls = True
+        self.actualRoot.useSacls = True
 
-        request = SimpleRequest(
-            self.site,
+        request = SimpleStoreRequest(
+            self,
             "GET",
             "/principals/",
-            headers=http_headers.Headers({
-                    "Authorization": ["basic", "%s" % (
-                            "dreid:dreid".encode("base64"),)]}))
+            headers=http_headers.Headers(
+                {
+                    "Authorization": [
+                        "basic", "%s" % ("dreid:dreid".encode("base64"),)
+                    ]
+                }
+            )
+        )
 
-        resrc, _ignore_segments = (yield maybeDeferred(
-            self.root.locateChild, request, ["principals"]
-        ))
-
         try:
             resrc, _ignore_segments = (yield maybeDeferred(
-                resrc.locateChild, request, ["principals"]
+                self.actualRoot.locateChild, request, ["principals"]
             ))
+            raise AssertionError(
+                "RootResource.locateChild did not return an error"
+            )
         except HTTPError, e:
             self.assertEquals(e.response.code, 401)
 
@@ -313,7 +262,7 @@
                 self.fail("Incorrect response for DELETE /: %s"
                           % (response.code,))
 
-        request = SimpleRequest(self.site, "DELETE", "/")
+        request = SimpleStoreRequest(self, "DELETE", "/")
         return self.send(request, do_test)
 
 
@@ -325,8 +274,8 @@
                 self.fail("Incorrect response for COPY /: %s"
                           % (response.code,))
 
-        request = SimpleRequest(
-            self.site,
+        request = SimpleStoreRequest(
+            self,
             "COPY",
             "/",
             headers=http_headers.Headers({"Destination": "/copy/"})
@@ -342,8 +291,8 @@
                 self.fail("Incorrect response for MOVE /: %s"
                           % (response.code,))
 
-        request = SimpleRequest(
-            self.site,
+        request = SimpleStoreRequest(
+            self,
             "MOVE",
             "/",
             headers=http_headers.Headers({"Destination": "/copy/"})
@@ -371,13 +320,15 @@
             return response
 
 
+    @inlineCallbacks
     def setUp(self):
-        super(SACLCacheTests, self).setUp()
-        self.root.resource.responseCache = SACLCacheTests.StubResponseCacheResource()
+        yield super(SACLCacheTests, self).setUp()
+        self.actualRoot.responseCache = SACLCacheTests.StubResponseCacheResource()
 
 
+    @inlineCallbacks
     def test_PROPFIND(self):
-        self.root.resource.useSacls = True
+        self.actualRoot.useSacls = True
 
         body = """<?xml version="1.0" encoding="utf-8" ?>
 <D:propfind xmlns:D="DAV:">
@@ -387,48 +338,46 @@
 </D:prop>
 </D:propfind>
 """
+        record = yield self.directory.recordWithShortName(
+            RecordType.user,
+            u"dreid"
+        )
 
-        request = SimpleRequest(
-            self.site,
+        request = SimpleStoreRequest(
+            self,
             "PROPFIND",
             "/principals/users/dreid/",
             headers=http_headers.Headers({
-                    'Authorization': ['basic', '%s' % ('dreid:dierd'.encode('base64'),)],
-                    'Content-Type': 'application/xml; charset="utf-8"',
                     'Depth': '1',
             }),
+            authRecord=record,
             content=body
         )
+        response = yield self.send(request)
+        response = IResponse(response)
 
-        def gotResponse1(response):
-            if response.code != responsecode.MULTI_STATUS:
-                self.fail("Incorrect response for PROPFIND /principals/: %s" % (response.code,))
+        if response.code != responsecode.MULTI_STATUS:
+            self.fail("Incorrect response for PROPFIND /principals/: %s" % (response.code,))
 
-            request = SimpleRequest(
-                self.site,
-                "PROPFIND",
-                "/principals/users/dreid/",
-                headers=http_headers.Headers({
-                        'Authorization': ['basic', '%s' % ('dreid:dierd'.encode('base64'),)],
-                        'Content-Type': 'application/xml; charset="utf-8"',
-                        'Depth': '1',
-                }),
-                content=body
-            )
+        request = SimpleStoreRequest(
+            self,
+            "PROPFIND",
+            "/principals/users/dreid/",
+            headers=http_headers.Headers({
+                    'Depth': '1',
+            }),
+            authRecord=record,
+            content=body
+        )
+        response = yield self.send(request)
+        response = IResponse(response)
 
-            d = self.send(request, gotResponse2)
-            return d
+        if response.code != responsecode.MULTI_STATUS:
+            self.fail("Incorrect response for PROPFIND /principals/: %s" % (response.code,))
+        self.assertEqual(self.actualRoot.responseCache.cacheHitCount, 1)
 
-        def gotResponse2(response):
-            if response.code != responsecode.MULTI_STATUS:
-                self.fail("Incorrect response for PROPFIND /principals/: %s" % (response.code,))
-            self.assertEqual(self.root.resource.responseCache.cacheHitCount, 1)
 
-        d = self.send(request, gotResponse1)
-        return d
 
-
-
 class WikiTests(RootTests):
 
     @inlineCallbacks
@@ -438,12 +387,9 @@
         request.checkedWiki will be set to True
         """
 
-        request = SimpleRequest(self.site, "GET", "/principals/")
+        request = SimpleStoreRequest(self, "GET", "/principals/")
 
         resrc, _ignore_segments = (yield maybeDeferred(
-            self.root.locateChild, request, ["principals"]
+            self.actualRoot.locateChild, request, ["principals"]
         ))
-        resrc, _ignore_segments = (yield maybeDeferred(
-            resrc.locateChild, request, ["principals"]
-        ))
         self.assertTrue(request.checkedWiki)

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/push/applepush.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/push/applepush.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/push/applepush.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -820,23 +820,25 @@
 
 
     def defaultAccessControlList(self):
-        return davxml.ACL(
-            # DAV:Read for authenticated principals
-            davxml.ACE(
-                davxml.Principal(davxml.Authenticated()),
-                davxml.Grant(
-                    davxml.Privilege(davxml.Read()),
+        return succeed(
+            davxml.ACL(
+                # DAV:Read for authenticated principals
+                davxml.ACE(
+                    davxml.Principal(davxml.Authenticated()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                    ),
+                    davxml.Protected(),
                 ),
-                davxml.Protected(),
-            ),
-            # DAV:Write for authenticated principals
-            davxml.ACE(
-                davxml.Principal(davxml.Authenticated()),
-                davxml.Grant(
-                    davxml.Privilege(davxml.Write()),
+                # DAV:Write for authenticated principals
+                davxml.ACE(
+                    davxml.Principal(davxml.Authenticated()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Write()),
+                    ),
+                    davxml.Protected(),
                 ),
-                davxml.Protected(),
-            ),
+            )
         )
 
 
@@ -869,6 +871,7 @@
 
     http_GET = http_POST
 
+    @inlineCallbacks
     def principalFromRequest(self, request):
         """
         Given an authenticated request, return the principal based on
@@ -877,9 +880,9 @@
         principal = None
         for collection in self.principalCollections():
             data = request.authnUser.children[0].children[0].data
-            principal = collection._principalForURI(data)
+            principal = yield collection._principalForURI(data)
             if principal is not None:
-                return principal
+                returnValue(principal)
 
 
     @inlineCallbacks
@@ -910,7 +913,7 @@
             msg = "Invalid request: bad 'token' %s" % (token,)
 
         else:
-            principal = self.principalFromRequest(request)
+            principal = yield self.principalFromRequest(request)
             uid = principal.record.uid
             try:
                 yield self.addSubscription(token, key, uid, userAgent, host)

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/caldav.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/caldav.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -90,17 +90,17 @@
 from txdav.common.datastore.work.revision_cleanup import (
     scheduleFirstFindMinRevision
 )
-from txdav.dps.server import DirectoryProxyServiceMaker
+from txdav.who.util import directoryFromConfig
 from txdav.dps.client import DirectoryService as DirectoryProxyClientService
-from txdav.who.groups import GroupCacher as NewGroupCacher
+from txdav.who.groups import GroupCacher
 
 from twistedcaldav import memcachepool
 from twistedcaldav.config import config, ConfigurationError
-from twistedcaldav.directory import calendaruserproxy
-from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
 from twistedcaldav.localization import processLocalizationFiles
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
-from twistedcaldav.upgrade import UpgradeFileSystemFormatStep, PostDBImportStep
+from twistedcaldav.upgrade import (
+    UpgradeFileSystemFormatStep, PostDBImportStep,
+)
 
 try:
     from twistedcaldav.authkerb import NegotiateCredentialFactory
@@ -124,6 +124,8 @@
     pgServiceFromConfig, getDBPool, MemoryLimitService,
     storeFromConfig
 )
+from twisted.application.strports import service as strPortsService
+from txdav.dps.server import DirectoryProxyAMPFactory
 
 try:
     from calendarserver.version import version
@@ -536,10 +538,7 @@
             )
             self.monitor.addProcessObject(process, PARENT_ENVIRONMENT)
 
-        if (
-           config.DirectoryProxy.Enabled and
-           config.DirectoryProxy.SocketPath != ""
-        ):
+        if config.DirectoryProxy.Enabled:
             log.info("Adding directory proxy service")
 
             dpsArgv = [
@@ -863,10 +862,10 @@
         CalDAV and CardDAV requests.
         """
         pool, txnFactory = getDBPool(config)
-        store = storeFromConfig(config, txnFactory)
+        directory = DirectoryProxyClientService(config.DirectoryRealmName)
+        store = storeFromConfig(config, txnFactory, directory)
         logObserver = AMPCommonAccessLoggingObserver()
         result = self.requestProcessingService(options, store, logObserver)
-        directory = store.directoryService()
 
         if pool is not None:
             pool.setServiceParent(result)
@@ -938,14 +937,9 @@
 
         # Optionally set up group cacher
         if config.GroupCaching.Enabled:
-            groupCacher = GroupMembershipCacheUpdater(
-                calendaruserproxy.ProxyDBService,
+            groupCacher = GroupCacher(
                 directory,
-                config.GroupCaching.UpdateSeconds,
-                config.GroupCaching.ExpireSeconds,
-                config.GroupCaching.LockSeconds,
-                namespace=config.GroupCaching.MemcachedPool,
-                useExternalProxies=config.GroupCaching.UseExternalProxies,
+                updateSeconds=config.GroupCaching.UpdateSeconds
             )
         else:
             groupCacher = None
@@ -1281,21 +1275,12 @@
 
             # Optionally set up group cacher
             if config.GroupCaching.Enabled:
-                groupCacher = GroupMembershipCacheUpdater(
-                    calendaruserproxy.ProxyDBService,
+                groupCacher = GroupCacher(
                     directory,
-                    config.GroupCaching.UpdateSeconds,
-                    config.GroupCaching.ExpireSeconds,
-                    config.GroupCaching.LockSeconds,
-                    namespace=config.GroupCaching.MemcachedPool,
-                    useExternalProxies=config.GroupCaching.UseExternalProxies
+                    updateSeconds=config.GroupCaching.UpdateSeconds
                 )
-                newGroupCacher = NewGroupCacher(
-                    DirectoryProxyClientService(None)
-                )
             else:
                 groupCacher = None
-                newGroupCacher = None
 
             # Optionally enable Manhole access
             if config.Manhole.Enabled:
@@ -1326,17 +1311,11 @@
                         "manhole_tap could not be imported"
                     )
 
-            # Optionally enable Directory Proxy
-            if config.DirectoryProxy.Enabled:
-                dps = DirectoryProxyServiceMaker().makeService(None)
-                dps.setServiceParent(result)
-
             def decorateTransaction(txn):
                 txn._pushDistributor = pushDistributor
                 txn._rootResource = result.rootResource
                 txn._mailRetriever = mailRetriever
                 txn._groupCacher = groupCacher
-                txn._newGroupCacher = newGroupCacher
 
             store.callWithNewTransactions(decorateTransaction)
 
@@ -1376,7 +1355,7 @@
                 Popen(memcachedArgv)
 
         return self.storageService(
-            slaveSvcCreator, logObserver, uid=uid, gid=gid
+            slaveSvcCreator, logObserver, uid=uid, gid=gid, directory=None
         )
 
 
@@ -1392,7 +1371,8 @@
             return config.UtilityServiceClass(store)
 
         uid, gid = getSystemIDs(config.UserName, config.GroupName)
-        return self.storageService(toolServiceCreator, None, uid=uid, gid=gid)
+        return self.storageService(toolServiceCreator, None, uid=uid, gid=gid,
+                                   directory=None)
 
 
     def makeService_Agent(self, options):
@@ -1440,7 +1420,7 @@
 
 
     def storageService(
-        self, createMainService, logObserver, uid=None, gid=None
+        self, createMainService, logObserver, uid=None, gid=None, directory=None
     ):
         """
         If necessary, create a service to be started used for storage; for
@@ -1466,9 +1446,13 @@
             running as root (also the gid to chown Attachments to).
         @type gid: C{int}
 
+        @param directory: The directory service to use.
+        @type directory: L{IStoreDirectoryService} or None
+
         @return: the appropriate a service to start.
         @rtype: L{IService}
         """
+
         def createSubServiceFactory(
             dialect=POSTGRES_DIALECT, paramstyle='pyformat'
         ):
@@ -1480,7 +1464,14 @@
                     maxConnections=config.MaxDBConnectionsPerPool
                 )
                 cp.setServiceParent(ms)
-                store = storeFromConfig(config, cp.connection)
+                store = storeFromConfig(config, cp.connection, directory)
+                if directory is None:
+                    # Create a Directory Proxy "Server" service and hand it to
+                    # the store.
+                    # FIXME: right now the store passed *to* the directory is the
+                    # calendar/contacts data store, but for a multi-server deployment
+                    # it will need its own separate store.
+                    store.setDirectoryService(directoryFromConfig(config, store=store))
 
                 pps = PreProcessingService(
                     createMainService, cp, store, logObserver, storageService
@@ -1497,7 +1488,7 @@
 
                 # Still need this for Snow Leopard support
                 pps.addStep(
-                    UpgradeFileSystemFormatStep(config)
+                    UpgradeFileSystemFormatStep(config, store)
                 )
 
                 pps.addStep(
@@ -1605,7 +1596,7 @@
                     "Unknown database type {}".format(config.DBType)
                 )
         else:
-            store = storeFromConfig(config, None)
+            store = storeFromConfig(config, None, directory)
             return createMainService(None, store, logObserver, None)
 
 
@@ -1867,14 +1858,9 @@
 
             # Optionally set up group cacher
             if config.GroupCaching.Enabled:
-                groupCacher = GroupMembershipCacheUpdater(
-                    calendaruserproxy.ProxyDBService,
+                groupCacher = GroupCacher(
                     directory,
-                    config.GroupCaching.UpdateSeconds,
-                    config.GroupCaching.ExpireSeconds,
-                    config.GroupCaching.LockSeconds,
-                    namespace=config.GroupCaching.MemcachedPool,
-                    useExternalProxies=config.GroupCaching.UseExternalProxies
+                    updateSeconds=config.GroupCaching.UpdateSeconds
                 )
             else:
                 groupCacher = None
@@ -1887,9 +1873,30 @@
 
             store.callWithNewTransactions(decorateTransaction)
 
+            # Set up AMP for DPS Server in the master instead of sidecar
+            if not config.DirectoryProxy.Enabled:
+                strPortsService(
+                    "unix:{path}:mode=660".format(
+                        path=config.DirectoryProxy.SocketPath
+                    ),
+                    DirectoryProxyAMPFactory(store.directoryService())
+                ).setServiceParent(multi)
+
             return multi
 
-        ssvc = self.storageService(spawnerSvcCreator, None, uid, gid)
+        if config.DirectoryProxy.Enabled:
+            # If the master is to act as a DPS client, and talk to the
+            # DPS sidecar:
+            directory = DirectoryProxyClientService(
+                config.DirectoryRealmName
+            )
+        else:
+            # If the master is to act as the DPS server:
+            directory = None
+
+        ssvc = self.storageService(
+            spawnerSvcCreator, None, uid, gid, directory=directory
+        )
         ssvc.setServiceParent(s)
         return s
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_caldav.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_caldav.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_caldav.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -28,12 +28,11 @@
 from twisted.python.threadable import isInIOThread
 from twisted.internet.reactor import callFromThread
 from twisted.python.usage import Options, UsageError
-from twisted.python.reflect import namedAny
 from twisted.python.procutils import which
 
 from twisted.internet.interfaces import IProcessTransport, IReactorProcess
 from twisted.internet.protocol import ServerFactory
-from twisted.internet.defer import Deferred, inlineCallbacks, passthru, succeed
+from twisted.internet.defer import Deferred, inlineCallbacks, succeed
 from twisted.internet.task import Clock
 from twisted.internet import reactor
 from twisted.application.service import (IService, IServiceCollection,
@@ -42,15 +41,15 @@
 
 from twext.python.log import Logger
 from twext.python.filepath import CachingFilePath as FilePath
-from plistlib import writePlist #@UnresolvedImport
+from plistlib import writePlist  # @UnresolvedImport
 from txweb2.dav import auth
 from txweb2.log import LogWrapperResource
 from twext.internet.tcp import MaxAcceptTCPServer, MaxAcceptSSLServer
 
 from twistedcaldav.config import config, ConfigDict, ConfigurationError
+from twistedcaldav.resource import AuthenticationWrapper
 from twistedcaldav.stdconfig import DEFAULT_CONFIG
 
-from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
 
@@ -178,7 +177,7 @@
 
 
 
-class CalDAVOptionsTest (StoreTestCase):
+class CalDAVOptionsTest(StoreTestCase):
     """
     Test various parameters of our usage.Options subclass
     """
@@ -304,111 +303,6 @@
 
 
 
-class BaseServiceMakerTests(StoreTestCase):
-    """
-    Utility class for ServiceMaker tests.
-    """
-    configOptions = None
-
-    @inlineCallbacks
-    def setUp(self):
-        yield super(BaseServiceMakerTests, self).setUp()
-        self.options = TestCalDAVOptions()
-        self.options.parent = Options()
-        self.options.parent["gid"] = None
-        self.options.parent["uid"] = None
-        self.options.parent["nodaemon"] = None
-
-        self.config = ConfigDict(DEFAULT_CONFIG)
-
-        accountsFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/accounts.xml")
-        resourcesFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/resources.xml")
-        augmentsFile = os.path.join(sourceRoot, "twistedcaldav/directory/test/augments.xml")
-        pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
-
-        self.config["DirectoryService"] = {
-            "params": {"xmlFile": accountsFile},
-            "type": "twistedcaldav.directory.xmlfile.XMLDirectoryService"
-        }
-
-        self.config["ResourceService"] = {
-            "params": {"xmlFile": resourcesFile},
-        }
-
-        self.config["AugmentService"] = {
-            "params": {"xmlFiles": [augmentsFile]},
-            "type": "twistedcaldav.directory.augment.AugmentXMLDB"
-        }
-
-        self.config.UseDatabase = False
-        self.config.ServerRoot = self.mktemp()
-        self.config.ConfigRoot = "config"
-        self.config.ProcessType = "Single"
-        self.config.SSLPrivateKey = pemFile
-        self.config.SSLCertificate = pemFile
-        self.config.EnableSSL = True
-        self.config.Memcached.Pools.Default.ClientEnabled = False
-        self.config.Memcached.Pools.Default.ServerEnabled = False
-        self.config.DirectoryAddressBook.Enabled = False
-        self.config.UsePackageTimezones = True
-
-        if self.configOptions:
-            self.config.update(self.configOptions)
-
-        os.mkdir(self.config.ServerRoot)
-        os.mkdir(os.path.join(self.config.ServerRoot, self.config.DocumentRoot))
-        os.mkdir(os.path.join(self.config.ServerRoot, self.config.DataRoot))
-        os.mkdir(os.path.join(self.config.ServerRoot, self.config.ConfigRoot))
-
-        self.configFile = self.mktemp()
-
-        self.writeConfig()
-
-
-    def tearDown(self):
-        config.setDefaults(DEFAULT_CONFIG)
-        config.reset()
-
-
-    def writeConfig(self):
-        """
-        Flush self.config out to self.configFile
-        """
-        writePlist(self.config, self.configFile)
-
-
-    def makeService(self, patcher=passthru):
-        """
-        Create a service by calling into CalDAVServiceMaker with
-        self.configFile
-        """
-        self.options.parseOptions(["-f", self.configFile])
-
-        maker = CalDAVServiceMaker()
-        maker = patcher(maker)
-        return maker.makeService(self.options)
-
-
-    def getSite(self):
-        """
-        Get the server.Site from the service by finding the HTTPFactory.
-        """
-        service = self.makeService()
-        for listeningService in inServiceHierarchy(
-                service,
-                # FIXME: need a better predicate for 'is this really an HTTP
-                # factory' but this works for now.
-                # NOTE: in a database 'single' configuration, PostgresService
-                # will prevent the HTTP services from actually getting added to
-                # the hierarchy until the hierarchy has started.
-                # 'underlyingSite' assigned in caldav.py
-                lambda x: hasattr(x, 'underlyingSite')
-            ):
-            return listeningService.underlyingSite
-        raise RuntimeError("No site found.")
-
-
-
 def inServiceHierarchy(svc, predicate):
     """
     Find services in the service collection which satisfy the given predicate.
@@ -452,44 +346,72 @@
 
 
 
-class CalDAVServiceMakerTests(BaseServiceMakerTests):
-    """
-    Test the service maker's behavior
-    """
+# Tests for the various makeService_ flavors:
 
-    def test_makeServiceDispatcher(self):
-        """
-        Test the default options of the dispatching makeService
-        """
-        validServices = ["Slave", "Combined"]
+class CalDAVServiceMakerTestBase(StoreTestCase):
 
-        self.config["HTTPPort"] = 0
+    @inlineCallbacks
+    def setUp(self):
+        yield super(CalDAVServiceMakerTestBase, self).setUp()
+        self.options = TestCalDAVOptions()
+        self.options.parent = Options()
+        self.options.parent["gid"] = None
+        self.options.parent["uid"] = None
+        self.options.parent["nodaemon"] = None
 
-        for service in validServices:
-            self.config["ProcessType"] = service
-            self.writeConfig()
-            self.makeService()
 
-        self.config["ProcessType"] = "Unknown Service"
-        self.writeConfig()
-        self.assertRaises(UsageError, self.makeService)
+class CalDAVServiceMakerTestSingle(CalDAVServiceMakerTestBase):
 
+    def configure(self):
+        super(CalDAVServiceMakerTestSingle, self).configure()
+        config.ProcessType = "Single"
 
+    def test_makeService(self):
+        CalDAVServiceMaker().makeService(self.options)
+        # No error
+
+
+class CalDAVServiceMakerTestSlave(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(CalDAVServiceMakerTestSlave, self).configure()
+        config.ProcessType = "Slave"
+
+    def test_makeService(self):
+        CalDAVServiceMaker().makeService(self.options)
+        # No error
+
+
+class CalDAVServiceMakerTestUnknown(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(CalDAVServiceMakerTestUnknown, self).configure()
+        config.ProcessType = "Unknown"
+
+    def test_makeService(self):
+        self.assertRaises(UsageError, CalDAVServiceMaker().makeService, self.options)
+        # error
+
+
+
+class ModesOnUNIXSocketsTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(ModesOnUNIXSocketsTests, self).configure()
+        config.ProcessType = "Combined"
+        config.HTTPPort = 0
+        self.alternateGroup = determineAppropriateGroupID()
+        config.GroupName = grp.getgrgid(self.alternateGroup).gr_name
+        config.Stats.EnableUnixStatsSocket = True
+
+
     def test_modesOnUNIXSockets(self):
         """
         The logging and stats UNIX sockets that are bound as part of the
         'Combined' service hierarchy should have a secure mode specified: only
         the executing user should be able to open and send to them.
         """
-
-        self.config["HTTPPort"] = 0 # Don't conflict with the test above.
-        alternateGroup = determineAppropriateGroupID()
-        self.config.GroupName = grp.getgrgid(alternateGroup).gr_name
-
-        self.config["ProcessType"] = "Combined"
-        self.config.Stats.EnableUnixStatsSocket = True
-        self.writeConfig()
-        svc = self.makeService()
+        svc = CalDAVServiceMaker().makeService(self.options)
         for serviceName in [_CONTROL_SERVICE_NAME]:
             socketService = svc.getServiceNamed(serviceName)
             self.assertIsInstance(socketService, GroupOwnedUNIXServer)
@@ -498,7 +420,7 @@
                 m, int("660", 8),
                 "Wrong mode on %s: %s" % (serviceName, oct(m))
             )
-            self.assertEquals(socketService.gid, alternateGroup)
+            self.assertEquals(socketService.gid, self.alternateGroup)
         for serviceName in ["unix-stats"]:
             socketService = svc.getServiceNamed(serviceName)
             self.assertIsInstance(socketService, GroupOwnedUNIXServer)
@@ -507,84 +429,119 @@
                 m, int("660", 8),
                 "Wrong mode on %s: %s" % (serviceName, oct(m))
             )
-            self.assertEquals(socketService.gid, alternateGroup)
+            self.assertEquals(socketService.gid, self.alternateGroup)
 
 
+class ProcessMonitorTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(ProcessMonitorTests, self).configure()
+        config.ProcessType = "Combined"
+
     def test_processMonitor(self):
         """
         In the master, there should be exactly one
         L{DelayedStartupProcessMonitor} in the service hierarchy so that it
         will be started by startup.
         """
-        self.config["ProcessType"] = "Combined"
-        self.writeConfig()
         self.assertEquals(
             1,
             len(
                 list(inServiceHierarchy(
-                    self.makeService(),
+                    CalDAVServiceMaker().makeService(self.options),
                     lambda x: isinstance(x, DelayedStartupProcessMonitor)))
             )
         )
 
 
+
+class StoreQueuerSetInMasterTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(StoreQueuerSetInMasterTests, self).configure()
+        config.ProcessType = "Combined"
+
+
     def test_storeQueuerSetInMaster(self):
         """
         In the master, the store's queuer should be set to a
         L{PeerConnectionPool}, so that work can be distributed to other
         processes.
         """
-        self.config["ProcessType"] = "Combined"
-        self.writeConfig()
         class NotAStore(object):
             queuer = LocalQueuer(None)
+
             def __init__(self, directory):
                 self.directory = directory
+
             def newTransaction(self):
                 return None
+
             def callWithNewTransactions(self, x):
                 pass
+
             def directoryService(self):
                 return self.directory
+
+
         store = NotAStore(self.directory)
+
+
         def something(proposal):
             pass
+
         store.queuer.callWithNewProposals(something)
+
+
         def patch(maker):
             def storageServiceStandIn(createMainService, logObserver,
-                                      uid=None, gid=None):
+                                      uid=None, gid=None, directory=None):
                 pool = None
                 logObserver = None
                 storageService = None
-                svc = createMainService(pool, store, logObserver,
-                    storageService)
+                svc = createMainService(
+                    pool, store, logObserver, storageService
+                )
                 multi = MultiService()
                 svc.setServiceParent(multi)
                 return multi
             self.patch(maker, "storageService", storageServiceStandIn)
             return maker
-        self.makeService(patch)
+
+        maker = CalDAVServiceMaker()
+        maker = patch(maker)
+        maker.makeService(self.options)
         self.assertIsInstance(store.queuer, PeerConnectionPool)
         self.assertIn(something, store.queuer.proposalCallbacks)
 
 
 
-class SlaveServiceTest(BaseServiceMakerTests):
+
+
+
+
+class SlaveServiceTests(CalDAVServiceMakerTestBase):
     """
     Test various configurations of the Slave service
     """
 
-    configOptions = {
-        "HTTPPort": 8008,
-        "SSLPort": 8443,
-    }
+    def configure(self):
+        super(SlaveServiceTests, self).configure()
+        config.ProcessType = "Slave"
+        config.HTTPPort = 8008
+        config.SSLPort = 8443
+        pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
+        config.SSLPrivateKey = pemFile
+        config.SSLCertificate = pemFile
+        config.EnableSSL = True
 
+
     def test_defaultService(self):
         """
         Test the value of a Slave service in it's simplest
         configuration.
         """
-        service = self.makeService()
+        service = CalDAVServiceMaker().makeService(self.options)
 
         self.failUnless(
             IService(service),
@@ -606,11 +563,12 @@
         default TCP and SSL configuration
         """
         # Note: the listeners are bundled within a MultiService named "ConnectionService"
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
         expectedSubServices = dict((
-            (MaxAcceptTCPServer, self.config["HTTPPort"]),
-            (MaxAcceptSSLServer, self.config["SSLPort"]),
+            (MaxAcceptTCPServer, config.HTTPPort),
+            (MaxAcceptSSLServer, config.SSLPort),
         ))
 
         configuredSubServices = [(s.__class__, getattr(s, 'args', None))
@@ -632,7 +590,9 @@
         Test that the configuration of the SSLServer reflect the config file's
         SSL Private Key and SSL Certificate
         """
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
         sslService = None
         for s in service.services:
@@ -645,75 +605,115 @@
         context = sslService.args[2]
 
         self.assertEquals(
-            self.config["SSLPrivateKey"],
+            config.SSLPrivateKey,
             context.privateKeyFileName
         )
         self.assertEquals(
-            self.config["SSLCertificate"],
+            config.SSLCertificate,
             context.certificateFileName,
         )
 
 
+
+class NoSSLTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(NoSSLTests, self).configure()
+        config.ProcessType = "Slave"
+        config.HTTPPort = 8008
+        # pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
+        # config.SSLPrivateKey = pemFile
+        # config.SSLCertificate = pemFile
+        # config.EnableSSL = True
+
     def test_noSSL(self):
         """
         Test the single service to make sure there is no SSL Service when SSL
         is disabled
         """
-        del self.config["SSLPort"]
-        self.writeConfig()
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
-
         self.assertNotIn(
             internet.SSLServer,
             [s.__class__ for s in service.services]
         )
 
 
+class NoHTTPTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(NoHTTPTests, self).configure()
+        config.ProcessType = "Slave"
+        config.SSLPort = 8443
+        pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
+        config.SSLPrivateKey = pemFile
+        config.SSLCertificate = pemFile
+        config.EnableSSL = True
+
     def test_noHTTP(self):
         """
         Test the single service to make sure there is no TCPServer when
         HTTPPort is not configured
         """
-        del self.config["HTTPPort"]
-        self.writeConfig()
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
-
         self.assertNotIn(
             internet.TCPServer,
             [s.__class__ for s in service.services]
         )
 
 
+class SingleBindAddressesTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(SingleBindAddressesTests, self).configure()
+        config.ProcessType = "Slave"
+        config.HTTPPort = 8008
+        config.BindAddresses = ["127.0.0.1"]
+
     def test_singleBindAddresses(self):
         """
         Test that the TCPServer and SSLServers are bound to the proper address
         """
-        self.config.BindAddresses = ["127.0.0.1"]
-        self.writeConfig()
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
-
         for s in service.services:
             if isinstance(s, (internet.TCPServer, internet.SSLServer)):
                 self.assertEquals(s.kwargs["interface"], "127.0.0.1")
 
 
+class MultipleBindAddressesTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(MultipleBindAddressesTests, self).configure()
+        config.ProcessType = "Slave"
+        config.HTTPPort = 8008
+        config.SSLPort = 8443
+        pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
+        config.SSLPrivateKey = pemFile
+        config.SSLCertificate = pemFile
+        config.EnableSSL = True
+        config.BindAddresses = [
+            "127.0.0.1",
+            "10.0.0.2",
+            "172.53.13.123",
+        ]
+
     def test_multipleBindAddresses(self):
         """
         Test that the TCPServer and SSLServers are bound to the proper
         addresses.
         """
-        self.config.BindAddresses = [
-            "127.0.0.1",
-            "10.0.0.2",
-            "172.53.13.123",
-        ]
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
-        self.writeConfig()
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
-
         tcpServers = []
         sslServers = []
 
@@ -723,10 +723,10 @@
             elif isinstance(s, internet.SSLServer):
                 sslServers.append(s)
 
-        self.assertEquals(len(tcpServers), len(self.config.BindAddresses))
-        self.assertEquals(len(sslServers), len(self.config.BindAddresses))
+        self.assertEquals(len(tcpServers), len(config.BindAddresses))
+        self.assertEquals(len(sslServers), len(config.BindAddresses))
 
-        for addr in self.config.BindAddresses:
+        for addr in config.BindAddresses:
             for s in tcpServers:
                 if s.kwargs["interface"] == addr:
                     tcpServers.remove(s)
@@ -739,13 +739,32 @@
         self.assertEquals(len(sslServers), 0)
 
 
+
+class ListenBacklogTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(ListenBacklogTests, self).configure()
+        config.ProcessType = "Slave"
+        config.ListenBacklog = 1024
+        config.HTTPPort = 8008
+        config.SSLPort = 8443
+        pemFile = os.path.join(sourceRoot, "twistedcaldav/test/data/server.pem")
+        config.SSLPrivateKey = pemFile
+        config.SSLCertificate = pemFile
+        config.EnableSSL = True
+        config.BindAddresses = [
+            "127.0.0.1",
+            "10.0.0.2",
+            "172.53.13.123",
+        ]
+
     def test_listenBacklog(self):
         """
         Test that the backlog arguments is set in TCPServer and SSLServers
         """
-        self.config.ListenBacklog = 1024
-        self.writeConfig()
-        service = self.makeService().getServiceNamed(CalDAVService.connectionServiceName)
+        # Note: the listeners are bundled within a MultiService named "ConnectionService"
+        service = CalDAVServiceMaker().makeService(self.options)
+        service = service.getServiceNamed(CalDAVService.connectionServiceName)
 
         for s in service.services:
             if isinstance(s, (internet.TCPServer, internet.SSLServer)):
@@ -753,32 +772,31 @@
 
 
 
-class ServiceHTTPFactoryTests(BaseServiceMakerTests):
-    """
-    Test the configuration of the initial resource hierarchy of the
-    single service
-    """
-    configOptions = {"HTTPPort": 8008}
+class AuthWrapperAllEnabledTests(CalDAVServiceMakerTestBase):
 
+    def configure(self):
+        super(AuthWrapperAllEnabledTests, self).configure()
+        config.HTTPPort = 8008
+        config.Authentication.Digest.Enabled = True
+        config.Authentication.Kerberos.Enabled = True
+        config.Authentication.Kerberos.ServicePrincipal = "http/hello at bob"
+        config.Authentication.Basic.Enabled = True
+
+
     def test_AuthWrapperAllEnabled(self):
         """
         Test the configuration of the authentication wrapper
         when all schemes are enabled.
         """
-        self.config.Authentication.Digest.Enabled = True
-        self.config.Authentication.Kerberos.Enabled = True
-        self.config.Authentication.Kerberos.ServicePrincipal = "http/hello at bob"
-        self.config.Authentication.Basic.Enabled = True
 
-        self.writeConfig()
-        site = self.getSite()
+        authWrapper = self.rootResource.resource
+        self.failUnless(
+            isinstance(
+                authWrapper,
+                auth.AuthenticationWrapper
+            )
+        )
 
-        self.failUnless(isinstance(
-                site.resource.resource,
-                auth.AuthenticationWrapper))
-
-        authWrapper = site.resource.resource
-
         expectedSchemes = ["negotiate", "digest", "basic"]
 
         for scheme in authWrapper.credentialFactories:
@@ -787,36 +805,39 @@
         self.assertEquals(len(expectedSchemes),
                           len(authWrapper.credentialFactories))
 
+        ncf = authWrapper.credentialFactories["negotiate"]
 
+        self.assertEquals(ncf.service, "http at HELLO")
+        self.assertEquals(ncf.realm, "bob")
+
+
+
+class ServicePrincipalNoneTests(CalDAVServiceMakerTestBase):
+
+    def configure(self):
+        super(ServicePrincipalNoneTests, self).configure()
+        config.HTTPPort = 8008
+        config.Authentication.Digest.Enabled = True
+        config.Authentication.Kerberos.Enabled = True
+        config.Authentication.Kerberos.ServicePrincipal = ""
+        config.Authentication.Basic.Enabled = True
+
     def test_servicePrincipalNone(self):
         """
         Test that the Kerberos principal look is attempted if the principal is empty.
         """
-        self.config.Authentication.Kerberos.ServicePrincipal = ""
-        self.config.Authentication.Kerberos.Enabled = True
-        self.writeConfig()
-        site = self.getSite()
-
-        authWrapper = site.resource.resource
-
+        authWrapper = self.rootResource.resource
         self.assertFalse("negotiate" in authWrapper.credentialFactories)
 
 
-    def test_servicePrincipal(self):
-        """
-        Test that the kerberos realm is the realm portion of a principal
-        in the form proto/host at realm
-        """
-        self.config.Authentication.Kerberos.ServicePrincipal = "http/hello at bob"
-        self.config.Authentication.Kerberos.Enabled = True
-        self.writeConfig()
-        site = self.getSite()
 
-        authWrapper = site.resource.resource
-        ncf = authWrapper.credentialFactories["negotiate"]
+class AuthWrapperPartialEnabledTests(CalDAVServiceMakerTestBase):
 
-        self.assertEquals(ncf.service, "http at HELLO")
-        self.assertEquals(ncf.realm, "bob")
+    def configure(self):
+        super(AuthWrapperPartialEnabledTests, self).configure()
+        config.Authentication.Digest.Enabled = True
+        config.Authentication.Kerberos.Enabled = False
+        config.Authentication.Basic.Enabled = False
 
 
     def test_AuthWrapperPartialEnabled(self):
@@ -826,14 +847,7 @@
         enabled.
         """
 
-        self.config.Authentication.Basic.Enabled = False
-        self.config.Authentication.Kerberos.Enabled = False
-
-        self.writeConfig()
-        site = self.getSite()
-
-        authWrapper = site.resource.resource
-
+        authWrapper = self.rootResource.resource
         expectedSchemes = ["digest"]
 
         for scheme in authWrapper.credentialFactories:
@@ -845,102 +859,67 @@
         )
 
 
+
+
+
+class ResourceTests(CalDAVServiceMakerTestBase):
+
     def test_LogWrapper(self):
         """
         Test the configuration of the log wrapper
         """
-        site = self.getSite()
+        self.failUnless(isinstance(self.rootResource, LogWrapperResource))
 
-        self.failUnless(isinstance(
-                site.resource,
-                LogWrapperResource))
 
+    def test_AuthWrapper(self):
+        """
+        Test the configuration of the auth wrapper
+        """
+        self.failUnless(isinstance(self.rootResource.resource, AuthenticationWrapper))
 
+
     def test_rootResource(self):
         """
         Test the root resource
         """
-        site = self.getSite()
-        root = site.resource.resource.resource
+        self.failUnless(isinstance(self.rootResource.resource.resource, RootResource))
 
-        self.failUnless(isinstance(root, RootResource))
 
-
+    @inlineCallbacks
     def test_principalResource(self):
         """
         Test the principal resource
         """
-        site = self.getSite()
-        root = site.resource.resource.resource
-
         self.failUnless(isinstance(
-            root.getChild("principals"),
+            (yield self.actualRoot.getChild("principals")),
             DirectoryPrincipalProvisioningResource
         ))
 
 
+    @inlineCallbacks
     def test_calendarResource(self):
         """
         Test the calendar resource
         """
-        site = self.getSite()
-        root = site.resource.resource.resource
-
         self.failUnless(isinstance(
-            root.getChild("calendars"),
+            (yield self.actualRoot.getChild("calendars")),
             DirectoryCalendarHomeProvisioningResource
         ))
 
 
-
-class DirectoryServiceTest(BaseServiceMakerTests):
-    """
-    Tests of the directory service
-    """
-
-    configOptions = {"HTTPPort": 8008}
-
+    @inlineCallbacks
     def test_sameDirectory(self):
         """
         Test that the principal hierarchy has a reference
         to the same DirectoryService as the calendar hierarchy
         """
-        site = self.getSite()
-        principals = site.resource.resource.resource.getChild("principals")
-        calendars = site.resource.resource.resource.getChild("calendars")
+        principals = yield self.actualRoot.getChild("principals")
+        calendars = yield self.actualRoot.getChild("calendars")
 
         self.assertEquals(principals.directory, calendars.directory)
 
 
-    def test_aggregateDirectory(self):
-        """
-        Assert that the base directory service is actually
-        an AggregateDirectoryService
-        """
-        site = self.getSite()
-        principals = site.resource.resource.resource.getChild("principals")
-        directory = principals.directory
 
-        self.failUnless(isinstance(directory, AggregateDirectoryService))
-
-
-    def test_configuredDirectoryService(self):
-        """
-        Test that the real directory service is the directory service
-        set in the configuration file.
-        """
-        site = self.getSite()
-        principals = site.resource.resource.resource.getChild("principals")
-        directory = principals.directory
-
-        realDirectory = directory.serviceForRecordType("users")
-
-        configuredDirectory = namedAny(self.config.DirectoryService.type)
-
-        self.failUnless(isinstance(realDirectory, configuredDirectory))
-
-
-
 class DummyProcessObject(object):
     """
     Simple stub for Process Object API which just has an executable and some
@@ -1012,9 +991,13 @@
         at once, to avoid resource exhaustion.
         """
         dspm = DelayedStartupProcessMonitor()
-        dspm.addProcessObject(ScriptProcessObject(
-                'longlines.py', str(DelayedStartupLineLogger.MAX_LENGTH)),
-                          os.environ)
+        dspm.addProcessObject(
+            ScriptProcessObject(
+                'longlines.py',
+                str(DelayedStartupLineLogger.MAX_LENGTH)
+            ),
+            os.environ
+        )
         dspm.startService()
         self.addCleanup(dspm.stopService)
 
@@ -1038,10 +1021,11 @@
         logging.addObserver(tempObserver)
         self.addCleanup(logging.removeObserver, tempObserver)
         d = Deferred()
+
         def assertions(result):
             self.assertEquals(["[Dummy] x",
                                "[Dummy] y",
-                               "[Dummy] y", # final segment
+                               "[Dummy] y",  # final segment
                                "[Dummy] z"],
                               [''.join(evt['message'])[:len('[Dummy]') + 2]
                                for evt in logged])
@@ -1066,20 +1050,27 @@
             ("a", ["a"]),
             ("abcde", ["abcde"]),
             ("abcdefghij", ["abcdefghij"]),
-            ("abcdefghijk",
-                ["abcdefghij (truncated, continued)",
-                 "k"
+            (
+                "abcdefghijk",
+                [
+                    "abcdefghij (truncated, continued)",
+                    "k"
                 ]
             ),
-            ("abcdefghijklmnopqrst",
-                ["abcdefghij (truncated, continued)",
-                 "klmnopqrst"
+            (
+                "abcdefghijklmnopqrst",
+                [
+                    "abcdefghij (truncated, continued)",
+                    "klmnopqrst"
                 ]
             ),
-            ("abcdefghijklmnopqrstuv",
-                ["abcdefghij (truncated, continued)",
-                 "klmnopqrst (truncated, continued)",
-                 "uv"]
+            (
+                "abcdefghijklmnopqrstuv",
+                [
+                    "abcdefghij (truncated, continued)",
+                    "klmnopqrst (truncated, continued)",
+                    "uv"
+                ]
             ),
         ]:
             self.assertEquals(output, testLogger._breakLineIntoSegments(input))
@@ -1282,9 +1273,10 @@
         twistd = which("twistd")[0]
         deferred = Deferred()
         proc = reactor.spawnProcess(
-            CapturingProcessProtocol(deferred, None), twistd,
-                [twistd, reactorArg, '-n', '-y', tacFilePath],
-                env=os.environ
+            CapturingProcessProtocol(deferred, None),
+            twistd,
+            [twistd, reactorArg, '-n', '-y', tacFilePath],
+            env=os.environ
         )
         reactor.callLater(3, proc.signalProcess, "HUP")
         reactor.callLater(6, proc.signalProcess, "TERM")
@@ -1325,14 +1317,15 @@
         def _getgid():
             return 45
 
-        return type(getSystemIDs)(getSystemIDs.func_code,
+        return type(getSystemIDs)(
+            getSystemIDs.func_code,
             {
-                "getpwnam" : _getpwnam,
-                "getgrnam" : _getgrnam,
-                "getuid" : _getuid,
-                "getgid" : _getgid,
-                "KeyError" : KeyError,
-                "ConfigurationError" : ConfigurationError,
+                "getpwnam": _getpwnam,
+                "getgrnam": _getgrnam,
+                "getuid": _getuid,
+                "getgid": _getgid,
+                "KeyError": KeyError,
+                "ConfigurationError": ConfigurationError,
             }
         )
 
@@ -1342,8 +1335,10 @@
         If userName is passed in but is not found on the system, raise a
         ConfigurationError
         """
-        self.assertRaises(ConfigurationError, self._wrappedFunction(),
-            "nonexistent", "exists")
+        self.assertRaises(
+            ConfigurationError, self._wrappedFunction(),
+            "nonexistent", "exists"
+        )
 
 
     def test_getSystemIDs_GroupNameNotFound(self):
@@ -1351,8 +1346,10 @@
         If groupName is passed in but is not found on the system, raise a
         ConfigurationError
         """
-        self.assertRaises(ConfigurationError, self._wrappedFunction(),
-            "exists", "nonexistent")
+        self.assertRaises(
+            ConfigurationError, self._wrappedFunction(),
+            "exists", "nonexistent"
+        )
 
 
     def test_getSystemIDs_NamesNotSpecified(self):
@@ -1428,8 +1425,10 @@
     def setUp(self):
         self.history = []
         self.clock = Clock()
-        self.pps = PreProcessingService(self.fakeServiceCreator, None, "store",
-            None, "storageService", reactor=self.clock)
+        self.pps = PreProcessingService(
+            self.fakeServiceCreator, None, "store",
+            None, "storageService", reactor=self.clock
+        )
 
 
     def _record(self, value, failure):
@@ -1447,9 +1446,13 @@
             StepFour(self._record, False)
         )
         self.pps.startService()
-        self.assertEquals(self.history,
-            ['one success', 'two success', 'three success', 'four success',
-            ('serviceCreator', 'store', 'storageService')])
+        self.assertEquals(
+            self.history,
+            [
+                'one success', 'two success', 'three success', 'four success',
+                ('serviceCreator', 'store', 'storageService')
+            ]
+        )
 
 
     def test_allFailure(self):
@@ -1463,9 +1466,13 @@
             StepFour(self._record, True)
         )
         self.pps.startService()
-        self.assertEquals(self.history,
-            ['one success', 'two failure', 'three failure', 'four failure',
-            ('serviceCreator', None, 'storageService')])
+        self.assertEquals(
+            self.history,
+            [
+                'one success', 'two failure', 'three failure', 'four failure',
+                ('serviceCreator', None, 'storageService')
+            ]
+        )
 
 
     def test_partialFailure(self):
@@ -1479,9 +1486,13 @@
             StepFour(self._record, False)
         )
         self.pps.startService()
-        self.assertEquals(self.history,
-            ['one success', 'two failure', 'three success', 'four failure',
-            ('serviceCreator', 'store', 'storageService')])
+        self.assertEquals(
+            self.history,
+            [
+                'one success', 'two failure', 'three success', 'four failure',
+                ('serviceCreator', 'store', 'storageService')
+            ]
+        )
 
 
     def test_quitAfterUpgradeStep(self):
@@ -1498,9 +1509,13 @@
         )
         triggerFile.setContent("")
         self.pps.startService()
-        self.assertEquals(self.history,
-            ['one success', 'two success', 'four failure',
-            ('serviceCreator', None, 'storageService')])
+        self.assertEquals(
+            self.history,
+            [
+                'one success', 'two success', 'four failure',
+                ('serviceCreator', None, 'storageService')
+            ]
+        )
         self.assertFalse(triggerFile.exists())
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/test/test_util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -14,11 +14,9 @@
 # limitations under the License.
 ##
 
-from calendarserver.tap.util import directoryFromConfig, MemoryLimitService, Stepper
+from calendarserver.tap.util import MemoryLimitService, Stepper
 from twistedcaldav.util import computeProcessCount
 from twistedcaldav.test.util import TestCase
-from twistedcaldav.config import config
-from twistedcaldav.directory.augment import AugmentXMLDB
 from twisted.internet.task import Clock
 from twisted.internet.defer import succeed, inlineCallbacks
 
@@ -55,21 +53,7 @@
 
 
 
-class UtilTestCase(TestCase):
 
-    def test_directoryFromConfig(self):
-        """
-        Ensure augments service is on by default
-        """
-        dir = directoryFromConfig(config)
-        for service in dir._recordTypes.values():
-            # all directory services belonging to the aggregate have
-            # augmentService set to AugmentXMLDB
-            if hasattr(service, "augmentService"):
-                self.assertTrue(isinstance(service.augmentService, AugmentXMLDB))
-
-
-
 # Stub classes for MemoryLimitServiceTestCase
 
 class StubProtocol(object):

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tap/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -28,7 +28,6 @@
 
 import errno
 import os
-from time import sleep
 from socket import fromfd, AF_UNIX, SOCK_STREAM, socketpair
 import psutil
 
@@ -36,6 +35,7 @@
 from twext.python.log import Logger
 from txweb2.auth.basic import BasicCredentialFactory
 from txweb2.dav import auth
+from txweb2.dav.util import joinURL
 from txweb2.http_headers import Headers
 from txweb2.resource import Resource
 from txweb2.static import File as FileResource
@@ -46,32 +46,26 @@
 from twisted.internet import reactor as _reactor
 from twisted.internet.reactor import addSystemEventTrigger
 from twisted.internet.tcp import Connection
-from twisted.python.reflect import namedClass
-# from twisted.python.failure import Failure
 
+from calendarserver.push.applepush import APNSubscriptionResource
+from calendarserver.push.notifier import NotifierFactory
+from twext.enterprise.adbapi2 import ConnectionPool, ConnectionPoolConnection
+from twext.enterprise.ienterprise import ORACLE_DIALECT
+from twext.enterprise.ienterprise import POSTGRES_DIALECT
 from twistedcaldav.bind import doBind
 from twistedcaldav.cache import CacheStoreNotifierFactory
-from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
-from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
 from twistedcaldav.directory.digest import QopDigestCredentialFactory
-from twistedcaldav.directory.directory import GroupMembershipCache
 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-from twistedcaldav.directory.wiki import WikiDirectoryService
-from calendarserver.push.notifier import NotifierFactory
-from calendarserver.push.applepush import APNSubscriptionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.resource import AuthenticationWrapper
-from txdav.caldav.datastore.scheduling.ischedule.dkim import DKIMUtils, DomainKeyResource
-from txdav.caldav.datastore.scheduling.ischedule.resource import IScheduleInboxResource
 from twistedcaldav.simpleresource import SimpleResource, SimpleRedirectResource
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav.timezoneservice import TimezoneServiceResource
 from twistedcaldav.timezonestdservice import TimezoneStdServiceResource
-from twext.enterprise.ienterprise import POSTGRES_DIALECT
-from twext.enterprise.ienterprise import ORACLE_DIALECT
-from twext.enterprise.adbapi2 import ConnectionPool, ConnectionPoolConnection
+from txdav.caldav.datastore.scheduling.ischedule.dkim import DKIMUtils, DomainKeyResource
+from txdav.caldav.datastore.scheduling.ischedule.resource import IScheduleInboxResource
 
 
 try:
@@ -100,7 +94,10 @@
 from urllib import quote
 from twisted.python.usage import UsageError
 
-
+from twext.who.checker import UsernamePasswordCredentialChecker
+from twext.who.checker import HTTPDigestCredentialChecker
+from twisted.cred.error import UnauthorizedLogin
+from txweb2.dav.auth import IPrincipalCredentials
 log = Logger()
 
 
@@ -218,7 +215,7 @@
 
 
 
-def storeFromConfig(config, txnFactory, directoryService=None):
+def storeFromConfig(config, txnFactory, directoryService):
     """
     Produce an L{IDataStore} from the given configuration, transaction factory,
     and notifier factory.
@@ -236,17 +233,14 @@
     if config.EnableResponseCache and config.Memcached.Pools.Default.ClientEnabled:
         notifierFactories["cache"] = CacheStoreNotifierFactory()
 
-    if directoryService is None:
-        directoryService = directoryFromConfig(config)
-
     quota = config.UserQuota
     if quota == 0:
         quota = None
     if txnFactory is not None:
         if config.EnableSSL:
-            uri = "https://%s:%s" % (config.ServerHostName, config.SSLPort,)
+            uri = "https://{config.ServerHostName}:{config.SSLPort}".format(config=config)
         else:
-            uri = "http://%s:%s" % (config.ServerHostName, config.HTTPPort,)
+            uri = "https://{config.ServerHostName}:{config.HTTPPort}".format(config=config)
         attachments_uri = uri + "/calendars/__uids__/%(home)s/dropbox/%(dropbox_id)s/%(name)s"
         store = CommonSQLDataStore(
             txnFactory, notifierFactories,
@@ -281,96 +275,65 @@
 
 
 
-def directoryFromConfig(config):
-    """
-    Create an L{AggregateDirectoryService} from the given configuration.
-    """
-    #
-    # Setup the Augment Service
-    #
-    if config.AugmentService.type:
-        augmentClass = namedClass(config.AugmentService.type)
-        log.info("Configuring augment service of type: {augmentClass}",
-            augmentClass=augmentClass)
-        try:
-            augmentService = augmentClass(**config.AugmentService.params)
-        except IOError:
-            log.error("Could not start augment service")
-            raise
-    else:
-        augmentService = None
+# MOVE2WHO -- should we move this class somewhere else?
+class PrincipalCredentialChecker(object):
+    credentialInterfaces = (IPrincipalCredentials,)
 
-    #
-    # Setup the group membership cacher
-    #
-    if config.GroupCaching.Enabled:
-        groupMembershipCache = GroupMembershipCache(
-            config.GroupCaching.MemcachedPool,
-            expireSeconds=config.GroupCaching.ExpireSeconds)
-    else:
-        groupMembershipCache = None
+    @inlineCallbacks
+    def requestAvatarId(self, credentials):
+        credentials = IPrincipalCredentials(credentials)
 
-    #
-    # Setup the Directory
-    #
-    directories = []
+        if credentials.authnPrincipal is None:
+            raise UnauthorizedLogin(
+                "No such user: {user}".format(
+                    user=credentials.credentials.username
+                )
+        )
 
-    directoryClass = namedClass(config.DirectoryService.type)
-    principalResourceClass = DirectoryPrincipalProvisioningResource
+        # See if record is enabledForLogin
+        if not credentials.authnPrincipal.record.isLoginEnabled():
+            raise UnauthorizedLogin(
+                "User not allowed to log in: {user}".format(
+                    user=credentials.credentials.username
+                )
+            )
 
-    log.info("Configuring directory service of type: {directoryType}",
-        directoryType=config.DirectoryService.type)
+        # Handle Kerberos as a separate behavior
+        try:
+            from twistedcaldav.authkerb import NegotiateCredentials
+        except ImportError:
+            NegotiateCredentials = None
 
-    config.DirectoryService.params.augmentService = augmentService
-    config.DirectoryService.params.groupMembershipCache = groupMembershipCache
-    baseDirectory = directoryClass(config.DirectoryService.params)
+        if NegotiateCredentials and isinstance(credentials.credentials,
+                                               NegotiateCredentials):
+            # If we get here with Kerberos, then authentication has already succeeded
+            returnValue(
+                (
+                    credentials.authnPrincipal.principalURL(),
+                    credentials.authzPrincipal.principalURL(),
+                    credentials.authnPrincipal,
+                    credentials.authzPrincipal,
+                )
+            )
+        else:
+            if (yield credentials.authnPrincipal.record.verifyCredentials(credentials.credentials)):
+                returnValue(
+                    (
+                        credentials.authnPrincipal.principalURL(),
+                        credentials.authzPrincipal.principalURL(),
+                        credentials.authnPrincipal,
+                        credentials.authzPrincipal,
+                    )
+                )
+            else:
+                raise UnauthorizedLogin(
+                    "Incorrect credentials for user: {user}".format(
+                        user=credentials.credentials.username
+                    )
+                )
 
-    # Wait for the directory to become available
-    while not baseDirectory.isAvailable():
-        sleep(5)
 
-    directories.append(baseDirectory)
 
-    #
-    # Setup the Locations and Resources Service
-    #
-    if config.ResourceService.Enabled:
-        resourceClass = namedClass(config.ResourceService.type)
-
-        log.info("Configuring resource service of type: {resourceClass}",
-            resourceClass=resourceClass)
-
-        config.ResourceService.params.augmentService = augmentService
-        config.ResourceService.params.groupMembershipCache = groupMembershipCache
-        resourceDirectory = resourceClass(config.ResourceService.params)
-        resourceDirectory.realmName = baseDirectory.realmName
-        directories.append(resourceDirectory)
-
-    #
-    # Add wiki directory service
-    #
-    if config.Authentication.Wiki.Enabled:
-        wikiDirectory = WikiDirectoryService()
-        wikiDirectory.realmName = baseDirectory.realmName
-        directories.append(wikiDirectory)
-
-    directory = AggregateDirectoryService(directories, groupMembershipCache)
-
-    #
-    # Use system-wide realm on OSX
-    #
-    try:
-        import ServerFoundation
-        realmName = ServerFoundation.XSAuthenticator.defaultRealm().encode("utf-8")
-        directory.setRealm(realmName)
-    except ImportError:
-        pass
-    log.info("Setting up principal collection: {cls}", cls=principalResourceClass)
-    principalResourceClass("/principals/", directory)
-    return directory
-
-
-
 def getRootResource(config, newStore, resources=None):
     """
     Set up directory service and resource hierarchy based on config.
@@ -407,22 +370,26 @@
     addressBookResourceClass = DirectoryAddressBookHomeProvisioningResource
     directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
     apnSubscriptionResourceClass = APNSubscriptionResource
+    principalResourceClass = DirectoryPrincipalProvisioningResource
 
     directory = newStore.directoryService()
+    principalCollection = principalResourceClass("/principals/", directory)
 
     #
     # Setup the ProxyDB Service
     #
-    proxydbClass = namedClass(config.ProxyDBService.type)
 
-    log.info("Configuring proxydb service of type: {cls}", cls=proxydbClass)
+    # MOVE2WHO
+    # proxydbClass = namedClass(config.ProxyDBService.type)
 
-    try:
-        calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
-    except IOError:
-        log.error("Could not start proxydb service")
-        raise
+    # log.info("Configuring proxydb service of type: {cls}", cls=proxydbClass)
 
+    # try:
+    #     calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+    # except IOError:
+    #     log.error("Could not start proxydb service")
+    #     raise
+
     #
     # Configure the Site and Wrappers
     #
@@ -431,9 +398,11 @@
 
     portal = Portal(auth.DavRealm())
 
-    portal.registerChecker(directory)
+    portal.registerChecker(UsernamePasswordCredentialChecker(directory))
+    portal.registerChecker(HTTPDigestCredentialChecker(directory))
+    portal.registerChecker(PrincipalCredentialChecker())
 
-    realm = directory.realmName or ""
+    realm = directory.realmName.encode("utf-8") or ""
 
     log.info("Configuring authentication for realm: {realm}", realm=realm)
 
@@ -491,7 +460,7 @@
     #
     log.info("Setting up document root at: {root}", root=config.DocumentRoot)
 
-    principalCollection = directory.principalCollection
+    # principalCollection = directory.principalCollection
 
     if config.EnableCalDAV:
         log.info("Setting up calendar collection: {cls}", cls=calendarResourceClass)
@@ -509,13 +478,14 @@
             newStore,
         )
 
-        directoryPath = os.path.join(config.DocumentRoot, config.DirectoryAddressBook.name)
         if config.DirectoryAddressBook.Enabled and config.EnableSearchAddressBook:
             log.info("Setting up directory address book: {cls}",
                 cls=directoryBackedAddressBookResourceClass)
 
             directoryBackedAddressBookCollection = directoryBackedAddressBookResourceClass(
-                principalCollections=(principalCollection,)
+                principalCollections=(principalCollection,),
+                principalDirectory=directory,
+                uri=joinURL("/", config.DirectoryAddressBook.name, "/")
             )
             if _reactor._started:
                 directoryBackedAddressBookCollection.provisionDirectory()
@@ -523,6 +493,7 @@
                 addSystemEventTrigger("after", "startup", directoryBackedAddressBookCollection.provisionDirectory)
         else:
             # remove /directory from previous runs that may have created it
+            directoryPath = os.path.join(config.DocumentRoot, config.DirectoryAddressBook.name)
             try:
                 FilePath(directoryPath).remove()
                 log.info("Deleted: {path}", path=directoryPath)
@@ -712,6 +683,7 @@
     #
     # Configure ancillary data
     #
+    # MOVE2WHO
     log.info("Configuring authentication wrapper")
 
     overrides = {}

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/agent.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/agent.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/agent.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,121 +18,46 @@
 
 """
 A service spawned on-demand by launchd, meant to handle configuration requests
-from Server.app.  When a request comes in on the socket specified in the launchd
-agent.plist, launchd will run "caldavd -t Agent" which ends up creating this
-service.  Requests are made using HTTP POSTS to /gateway, and are authenticated
-by OpenDirectory.
+from Server.app.  When a request comes in on the socket specified in the
+launchd agent.plist, launchd will run "caldavd -t Agent" which ends up creating
+this service.  Requests are made using HTTP POSTS to /gateway, and are
+authenticated by OpenDirectory.
 """
 
 from __future__ import print_function
 
+__all__ = [
+    "makeAgentService",
+]
+
 import cStringIO
+from plistlib import readPlistFromString, writePlistToString
 import socket
 
 from calendarserver.tap.util import getRootResource
-from plistlib import readPlistFromString, writePlistToString
+from twext.python.launchd import getLaunchDSocketFDs
+from twext.python.log import Logger
+from twext.who.checker import HTTPDigestCredentialChecker
+from twext.who.opendirectory import (
+    DirectoryService as OpenDirectoryDirectoryService,
+    NoQOPDigestCredentialFactory
+)
 from twisted.application.internet import StreamServerEndpointService
-from twisted.cred.checkers import ICredentialsChecker
-from twisted.cred.credentials import IUsernameHashedPassword
-from twisted.cred.error import UnauthorizedLogin
 from twisted.cred.portal import IRealm, Portal
-from twisted.internet.defer import inlineCallbacks, returnValue, succeed, fail
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.endpoints import AdoptedStreamServerEndpoint
 from twisted.internet.protocol import Factory
 from twisted.protocols import amp
-from twisted.web.guard import HTTPAuthSessionWrapper, DigestCredentialFactory
+from twisted.web.guard import HTTPAuthSessionWrapper
 from twisted.web.resource import IResource, Resource, ForbiddenResource
 from twisted.web.server import Site, NOT_DONE_YET
 from zope.interface import implements
 
-from twext.python.launchd import getLaunchDSocketFDs
-from twext.python.log import Logger
+
 log = Logger()
 
 
 
-class DirectoryServiceChecker:
-    """
-    A checker that knows how to ask OpenDirectory to authenticate via Digest
-    """
-    implements(ICredentialsChecker)
-
-    credentialInterfaces = (IUsernameHashedPassword,)
-
-    from calendarserver.platform.darwin.od import opendirectory
-    directoryModule = opendirectory
-
-    def __init__(self, node):
-        """
-        @param node: the name of the OpenDirectory node to use, e.g. /Local/Default
-        """
-        self.node = node
-        self.directory = self.directoryModule.odInit(node)
-
-
-    def requestAvatarId(self, credentials):
-        record = self.directoryModule.getUserRecord(self.directory, credentials.username)
-
-        if record is not None:
-            try:
-                if "algorithm" not in credentials.fields:
-                    credentials.fields["algorithm"] = "md5"
-
-                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
-
-                response = (
-                    'Digest username="%(username)s", '
-                    'realm="%(realm)s", '
-                    'nonce="%(nonce)s", '
-                    'uri="%(uri)s", '
-                    'response="%(response)s",'
-                    'algorithm=%(algorithm)s'
-                ) % credentials.fields
-
-            except KeyError as e:
-                log.error(
-                    "OpenDirectory (node=%s) error while performing digest authentication for user %s: "
-                    "missing digest response field: %s in: %s"
-                    % (self.node, credentials.username, e, credentials.fields)
-                )
-                return fail(UnauthorizedLogin())
-
-            try:
-                if self.directoryModule.authenticateUserDigest(self.directory,
-                    self.node,
-                    credentials.username,
-                    challenge,
-                    response,
-                    credentials.method
-                ):
-                    return succeed(credentials.username)
-                else:
-                    log.error("Failed digest auth with response: %s" % (response,))
-                    return fail(UnauthorizedLogin())
-            except Exception as e:
-                log.error(
-                    "OpenDirectory error while performing digest authentication for user %s: %s"
-                    % (credentials.username, e)
-                )
-                return fail(UnauthorizedLogin())
-
-        else:
-            return fail(UnauthorizedLogin())
-
-
-
-class CustomDigestCredentialFactory(DigestCredentialFactory):
-    """
-    DigestCredentialFactory without qop, to interop with OD.
-    """
-
-    def getChallenge(self, address):
-        result = DigestCredentialFactory.getChallenge(self, address)
-        del result["qop"]
-        return result
-
-
-
 class AgentRealm(object):
     """
     Only allow a specified list of avatar IDs to access the site
@@ -161,7 +86,8 @@
 
 class AgentGatewayResource(Resource):
     """
-    The gateway resource which forwards incoming requests through gateway.Runner.
+    The gateway resource which forwards incoming requests through
+    gateway.Runner.
     """
     isLeaf = True
 
@@ -202,10 +128,10 @@
             tbString = tbStringIO.getvalue()
             tbStringIO.close()
             error = {
-                "Error" : message,
-                "Traceback" : tbString,
+                "Error": message,
+                "Traceback": tbString,
             }
-            log.error("command failed %s" % (failure,))
+            log.error("command failed {error}", error=failure)
             request.write(writePlistToString(error))
             request.finish()
 
@@ -213,8 +139,10 @@
         body = request.content.read()
         command = readPlistFromString(body)
         output = cStringIO.StringIO()
-        runner = Runner(self.davRootResource, self.directory, self.store,
-            [command], output=output)
+        runner = Runner(
+            self.davRootResource, self.directory, self.store,
+            [command], output=output
+        )
         d = runner.run()
         d.addCallback(onSuccess, output)
         d.addErrback(onError)
@@ -246,16 +174,26 @@
         log.warn("Agent inactive; shutting down")
         reactor.stop()
 
-    inactivityDetector = InactivityDetector(reactor,
-        config.AgentInactivityTimeoutSeconds, becameInactive)
+    inactivityDetector = InactivityDetector(
+        reactor, config.AgentInactivityTimeoutSeconds, becameInactive
+    )
     root = Resource()
-    root.putChild("gateway", AgentGatewayResource(store,
-        davRootResource, directory, inactivityDetector))
+    root.putChild(
+        "gateway",
+        AgentGatewayResource(
+            store, davRootResource, directory, inactivityDetector
+        )
+    )
 
-    realmName = "/Local/Default"
-    portal = Portal(AgentRealm(root, ["com.apple.calendarserver"]),
-        [DirectoryServiceChecker(realmName)])
-    credentialFactory = CustomDigestCredentialFactory("md5", realmName)
+    directory = OpenDirectoryDirectoryService("/Local/Default")
+
+    portal = Portal(
+        AgentRealm(root, ["com.apple.calendarserver"]),
+        [HTTPDigestCredentialChecker(directory)]
+    )
+    credentialFactory = NoQOPDigestCredentialFactory(
+        "md5", "CalendarServer Agent Realm"
+    )
     wrapper = HTTPAuthSessionWrapper(portal, [credentialFactory])
 
     site = Site(wrapper)
@@ -283,8 +221,10 @@
         self._becameInactive = becameInactive
 
         if self._timeoutSeconds > 0:
-            self._delayedCall = self._reactor.callLater(self._timeoutSeconds,
-                self._inactivityThresholdReached)
+            self._delayedCall = self._reactor.callLater(
+                self._timeoutSeconds,
+                self._inactivityThresholdReached
+            )
 
 
     def _inactivityThresholdReached(self):
@@ -304,8 +244,10 @@
             if self._delayedCall.active():
                 self._delayedCall.reset(self._timeoutSeconds)
             else:
-                self._delayedCall = self._reactor.callLater(self._timeoutSeconds,
-                    self._inactivityThresholdReached)
+                self._delayedCall = self._reactor.callLater(
+                    self._timeoutSeconds,
+                    self._inactivityThresholdReached
+                )
 
 
     def stop(self):
@@ -361,17 +303,17 @@
         command = readPlistFromString(command)
         output = cStringIO.StringIO()
         from calendarserver.tools.gateway import Runner
-        runner = Runner(self.davRootResource, self.directory, self.store,
-            [command], output=output)
+        runner = Runner(
+            self.davRootResource, self.directory, self.store,
+            [command], output=output
+        )
 
         try:
             yield runner.run()
             result = output.getvalue()
             output.close()
         except Exception as e:
-            error = {
-                "Error" : str(e),
-            }
+            error = {"Error": str(e)}
             result = writePlistToString(error)
 
         output.close()
@@ -396,8 +338,9 @@
 
 
     def buildProtocol(self, addr):
-        return GatewayAMPProtocol(self.store, self.davRootResource,
-            self.directory)
+        return GatewayAMPProtocol(
+            self.store, self.davRootResource, self.directory
+        )
 
 
 
@@ -406,7 +349,8 @@
 #
 
 command = """<?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">
+<!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>command</key>
@@ -414,6 +358,7 @@
 </dict>
 </plist>"""
 
+
 def getList():
     # For the sample client, below:
     from twisted.internet import reactor

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/calverify.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/calverify.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/calverify.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -47,41 +47,39 @@
 import traceback
 import uuid
 
+from calendarserver.tools import tables
+from calendarserver.tools.cmdline import utilityMain, WorkerService
+from pycalendar.datetime import DateTime
+from pycalendar.exceptions import ErrorBase
 from pycalendar.icalendar import definitions
 from pycalendar.icalendar.calendar import Calendar
-from pycalendar.datetime import DateTime
-from pycalendar.exceptions import ErrorBase
 from pycalendar.period import Period
 from pycalendar.timezone import Timezone
-
+from twext.enterprise.dal.syntax import Select, Parameter, Count
+from twext.python.log import Logger
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python import usage
 from twisted.python.usage import Options
-
-from twext.python.log import Logger
-from twext.enterprise.dal.syntax import Select, Parameter, Count
-
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.dateops import pyCalendarTodatetime
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.ical import Component, ignoredComponents, \
+from twistedcaldav.ical import (
+    Component, ignoredComponents,
     InvalidICalendarDataError, Property, PERUSER_COMPONENT
-from txdav.caldav.datastore.scheduling.itip import iTipGenerator
+)
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
-from twistedcaldav.util import normalizationLookup
-
-from txdav.caldav.icalendarstore import ComponentUpdateState
 from txdav.caldav.datastore.scheduling.icalsplitter import iCalSplitter
 from txdav.caldav.datastore.scheduling.implicit import ImplicitScheduler
+from txdav.caldav.datastore.scheduling.itip import iTipGenerator
 from txdav.caldav.datastore.sql import CalendarStoreFeatures
+from txdav.caldav.datastore.util import normalizationLookup
+from txdav.caldav.icalendarstore import ComponentUpdateState
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_OWN
 from txdav.common.icommondatastore import InternalDataStoreError
+from txdav.who.idirectory import (
+    RecordType as CalRecordType, AutoScheduleMode
+)
 
-from calendarserver.tools.cmdline import utilityMain, WorkerService
 
-from calendarserver.tools import tables
-from calendarserver.tools.util import getDirectory
-
 log = Logger()
 
 
@@ -396,7 +394,7 @@
         self.output = output
         self.reactor = reactor
         self.config = config
-        self._directory = None
+        self._directory = store.directoryService()
 
         self.cuaCache = {}
 
@@ -427,11 +425,8 @@
 
     def directoryService(self):
         """
-        Get an appropriate directory service for this L{CalVerifyService}'s
-        configuration, creating one first if necessary.
+        Return the directory service
         """
-        if self._directory is None:
-            self._directory = getDirectory(self.config) #directoryFromConfig(self.config)
         return self._directory
 
 
@@ -970,14 +965,14 @@
                 )).ljust(80))
                 self.output.flush()
 
-            record = self.directoryService().recordWithGUID(uid)
+            record = yield self.directoryService().recordWithUID(uid)
             if record is None:
                 contents = yield self.countHomeContents(uid)
                 missing.append((uid, contents,))
             elif not record.thisServer():
                 contents = yield self.countHomeContents(uid)
                 wrong_server.append((uid, contents,))
-            elif not record.enabledForCalendaring:
+            elif not record.hasCalendars:
                 contents = yield self.countHomeContents(uid)
                 disabled.append((uid, contents,))
 
@@ -1008,7 +1003,7 @@
         table = tables.Table()
         table.addHeader(("Owner UID", "Calendar Objects"))
         for uid, count in sorted(wrong_server, key=lambda x: x[0]):
-            record = self.directoryService().recordWithGUID(uid)
+            record = yield self.directoryService().recordWithUID(uid)
             table.addRow((
                 "%s/%s (%s)" % (record.recordType if record else "-", record.shortNames[0] if record else "-", uid,),
                 count,
@@ -1022,7 +1017,7 @@
         table = tables.Table()
         table.addHeader(("Owner UID", "Calendar Objects"))
         for uid, count in sorted(disabled, key=lambda x: x[0]):
-            record = self.directoryService().recordWithGUID(uid)
+            record = yield self.directoryService().recordWithUID(uid)
             table.addRow((
                 "%s/%s (%s)" % (record.recordType if record else "-", record.shortNames[0] if record else "-", uid,),
                 count,
@@ -1152,7 +1147,7 @@
         table.addHeader(("Owner", "Event UID", "RID", "Problem",))
         for item in sorted(results_bad, key=lambda x: (x[0], x[1])):
             owner, uid, resid, message = item
-            owner_record = self.directoryService().recordWithGUID(owner)
+            owner_record = yield self.directoryService().recordWithUID(owner)
             table.addRow((
                 "%s/%s (%s)" % (owner_record.recordType if owner_record else "-", owner_record.shortNames[0] if owner_record else "-", owner,),
                 uid,
@@ -1199,7 +1194,7 @@
                 component.validOrganizerForScheduling(doFix=False)
                 if component.hasDuplicateAlarms(doFix=False):
                     raise InvalidICalendarDataError("Duplicate VALARMS")
-            self.noPrincipalPathCUAddresses(component, doFix=False)
+            yield self.noPrincipalPathCUAddresses(component, doFix=False)
             if self.options["ical"]:
                 self.attendeesWithoutOrganizer(component, doFix=False)
 
@@ -1220,15 +1215,17 @@
         returnValue((result, message,))
 
 
+    @inlineCallbacks
     def noPrincipalPathCUAddresses(self, component, doFix):
 
-        def lookupFunction(cuaddr, principalFunction, conf):
+        @inlineCallbacks
+        def lookupFunction(cuaddr, recordFunction, conf):
 
             # Return cached results, if any.
             if cuaddr in self.cuaCache:
-                return self.cuaCache[cuaddr]
+                returnValue(self.cuaCache[cuaddr])
 
-            result = normalizationLookup(cuaddr, principalFunction, conf)
+            result = yield normalizationLookup(cuaddr, recordFunction, conf)
             _ignore_name, guid, _ignore_cuaddrs = result
             if guid is None:
                 if cuaddr.find("__uids__") != -1:
@@ -1237,7 +1234,7 @@
 
             # Cache the result
             self.cuaCache[cuaddr] = result
-            return result
+            returnValue(result)
 
         for subcomponent in component.subcomponents():
             if subcomponent.name() in ignoredComponents:
@@ -1249,13 +1246,13 @@
                 # http(s) principals need to be converted to urn:uuid
                 if cuaddr.startswith("http"):
                     if doFix:
-                        component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().principalForCalendarUserAddress)
+                        yield component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().recordWithCalendarUserAddress)
                     else:
                         raise InvalidICalendarDataError("iCalendar ORGANIZER starts with 'http(s)'")
                 elif cuaddr.startswith("mailto:"):
-                    if lookupFunction(cuaddr, self.directoryService().principalForCalendarUserAddress, self.config)[1] is not None:
+                    if (yield lookupFunction(cuaddr, self.directoryService().recordWithCalendarUserAddress, self.config))[1] is not None:
                         if doFix:
-                            component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().principalForCalendarUserAddress)
+                            yield component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().recordWithCalendarUserAddress)
                         else:
                             raise InvalidICalendarDataError("iCalendar ORGANIZER starts with 'mailto:' and record exists")
                 else:
@@ -1263,7 +1260,7 @@
                         if doFix:
                             # Add back in mailto: then re-normalize to urn:uuid if possible
                             organizer.setValue("mailto:%s" % (cuaddr,))
-                            component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().principalForCalendarUserAddress)
+                            yield component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().recordWithCalendarUserAddress)
 
                             # Remove any SCHEDULE-AGENT=NONE
                             if organizer.parameterValue("SCHEDULE-AGENT", "SERVER") == "NONE":
@@ -1286,13 +1283,13 @@
                 # http(s) principals need to be converted to urn:uuid
                 if cuaddr.startswith("http"):
                     if doFix:
-                        component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().principalForCalendarUserAddress)
+                        yield component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().recordWithCalendarUserAddress)
                     else:
                         raise InvalidICalendarDataError("iCalendar ATTENDEE starts with 'http(s)'")
                 elif cuaddr.startswith("mailto:"):
-                    if lookupFunction(cuaddr, self.directoryService().principalForCalendarUserAddress, self.config)[1] is not None:
+                    if (yield lookupFunction(cuaddr, self.directoryService().recordWithCalendarUserAddress, self.config))[1] is not None:
                         if doFix:
-                            component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().principalForCalendarUserAddress)
+                            yield component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().recordWithCalendarUserAddress)
                         else:
                             raise InvalidICalendarDataError("iCalendar ATTENDEE starts with 'mailto:' and record exists")
                 else:
@@ -1300,7 +1297,7 @@
                         if doFix:
                             # Add back in mailto: then re-normalize to urn:uuid if possible
                             attendee.setValue("mailto:%s" % (cuaddr,))
-                            component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().principalForCalendarUserAddress)
+                            yield component.normalizeCalendarUserAddresses(lookupFunction, self.directoryService().recordWithCalendarUserAddress)
                         else:
                             raise InvalidICalendarDataError("iCalendar ATTENDEE missing mailto:")
 
@@ -1352,7 +1349,7 @@
                 component.validCalendarForCalDAV(methodAllowed=isinbox)
                 component.validOrganizerForScheduling(doFix=True)
                 component.hasDuplicateAlarms(doFix=True)
-            self.noPrincipalPathCUAddresses(component, doFix=True)
+            yield self.noPrincipalPathCUAddresses(component, doFix=True)
             if self.options["ical"]:
                 self.attendeesWithoutOrganizer(component, doFix=True)
         except ValueError:
@@ -1460,7 +1457,7 @@
         self.attended = []
         self.attended_byuid = collections.defaultdict(list)
         self.matched_attendee_to_organizer = collections.defaultdict(set)
-        skipped, inboxes = self.buildResourceInfo(rows)
+        skipped, inboxes = yield self.buildResourceInfo(rows)
 
         self.logResult("Number of organizer events to process", len(self.organized), self.total)
         self.logResult("Number of attendee events to process", len(self.attended), self.total)
@@ -1488,6 +1485,7 @@
         self.printSummary()
 
 
+    @inlineCallbacks
     def buildResourceInfo(self, rows, onlyOrganizer=False, onlyAttendee=False):
         """
         For each resource, determine whether it is an organizer or attendee event, and also
@@ -1506,7 +1504,7 @@
         for owner, resid, uid, calname, md5, organizer, created, modified in rows:
 
             # Skip owners not enabled for calendaring
-            if not self.testForCalendaringUUID(owner):
+            if not (yield self.testForCalendaringUUID(owner)):
                 skipped += 1
                 continue
 
@@ -1530,9 +1528,10 @@
                     self.attended.append((owner, resid, uid, md5, organizer, created, modified,))
                     self.attended_byuid[uid].append((owner, resid, uid, md5, organizer, created, modified,))
 
-        return skipped, inboxes
+        returnValue((skipped, inboxes))
 
 
+    @inlineCallbacks
     def testForCalendaringUUID(self, uuid):
         """
         Determine if the specified directory UUID is valid for calendaring. Keep a cache of
@@ -1545,9 +1544,9 @@
         """
 
         if uuid not in self.validForCalendaringUUIDs:
-            record = self.directoryService().recordWithGUID(uuid)
-            self.validForCalendaringUUIDs[uuid] = record is not None and record.enabledForCalendaring and record.thisServer()
-        return self.validForCalendaringUUIDs[uuid]
+            record = yield self.directoryService().recordWithUID(uuid)
+            self.validForCalendaringUUIDs[uuid] = record is not None and record.hasCalendars and record.thisServer()
+        returnValue(self.validForCalendaringUUIDs[uuid])
 
 
     @inlineCallbacks
@@ -1622,7 +1621,7 @@
                 self.matched_attendee_to_organizer[uid].add(organizerAttendee)
 
                 # Skip attendees not enabled for calendaring
-                if not self.testForCalendaringUUID(organizerAttendee):
+                if not (yield self.testForCalendaringUUID(organizerAttendee)):
                     continue
 
                 # Double check the missing attendee situation in case we missed it during the original query
@@ -1699,8 +1698,8 @@
         results_missing.sort()
         for item in results_missing:
             uid, resid, organizer, attendee, created, modified = item
-            organizer_record = self.directoryService().recordWithGUID(organizer)
-            attendee_record = self.directoryService().recordWithGUID(attendee)
+            organizer_record = yield self.directoryService().recordWithUID(organizer)
+            attendee_record = yield self.directoryService().recordWithUID(attendee)
             table.addRow((
                 "%s/%s (%s)" % (organizer_record.recordType if organizer_record else "-", organizer_record.shortNames[0] if organizer_record else "-", organizer,),
                 "%s/%s (%s)" % (attendee_record.recordType if attendee_record else "-", attendee_record.shortNames[0] if attendee_record else "-", attendee,),
@@ -1721,8 +1720,8 @@
         results_mismatch.sort()
         for item in results_mismatch:
             uid, org_resid, organizer, org_created, org_modified, attendee, att_created, att_modified = item
-            organizer_record = self.directoryService().recordWithGUID(organizer)
-            attendee_record = self.directoryService().recordWithGUID(attendee)
+            organizer_record = yield self.directoryService().recordWithUID(organizer)
+            attendee_record = yield self.directoryService().recordWithUID(attendee)
             table.addRow((
                 "%s/%s (%s)" % (organizer_record.recordType if organizer_record else "-", organizer_record.shortNames[0] if organizer_record else "-", organizer,),
                 "%s/%s (%s)" % (attendee_record.recordType if attendee_record else "-", attendee_record.shortNames[0] if attendee_record else "-", attendee,),
@@ -1789,14 +1788,14 @@
             organizer = organizer[9:]
 
             # Skip organizers not enabled for calendaring
-            if not self.testForCalendaringUUID(organizer):
+            if not (yield self.testForCalendaringUUID(organizer)):
                 continue
 
             # Double check the missing attendee situation in case we missed it during the original query
             if uid not in self.organized_byuid:
                 # Try to reload the organizer info data
                 rows = yield self.getAllResourceInfoWithUID(uid)
-                self.buildResourceInfo(rows, onlyOrganizer=True)
+                yield self.buildResourceInfo(rows, onlyOrganizer=True)
 
                 #if uid in self.organized_byuid:
                 #    print("Reloaded missing organizer data: %s" % (uid,))
@@ -1849,9 +1848,9 @@
             uid, attendee, organizer, resid, created, modified = item
             unique_set.add(uid)
             if organizer:
-                organizerRecord = self.directoryService().recordWithGUID(organizer)
+                organizerRecord = yield self.directoryService().recordWithUID(organizer)
                 organizer = "%s/%s (%s)" % (organizerRecord.recordType if organizerRecord else "-", organizerRecord.shortNames[0] if organizerRecord else "-", organizer,)
-            attendeeRecord = self.directoryService().recordWithGUID(attendee)
+            attendeeRecord = yield self.directoryService().recordWithUID(attendee)
             table.addRow((
                 organizer,
                 "%s/%s (%s)" % (attendeeRecord.recordType if attendeeRecord else "-", attendeeRecord.shortNames[0] if attendeeRecord else "-", attendee,),
@@ -1874,9 +1873,9 @@
         for item in mismatched:
             uid, attendee, organizer, resid, att_created, att_modified = item
             if organizer:
-                organizerRecord = self.directoryService().recordWithGUID(organizer)
+                organizerRecord = yield self.directoryService().recordWithUID(organizer)
                 organizer = "%s/%s (%s)" % (organizerRecord.recordType if organizerRecord else "-", organizerRecord.shortNames[0] if organizerRecord else "-", organizer,)
-            attendeeRecord = self.directoryService().recordWithGUID(attendee)
+            attendeeRecord = yield self.directoryService().recordWithUID(attendee)
             table.addRow((
                 organizer,
                 "%s/%s (%s)" % (attendeeRecord.recordType if attendeeRecord else "-", attendeeRecord.shortNames[0] if attendeeRecord else "-", attendee,),
@@ -1980,8 +1979,9 @@
             self.txn = self.store.newTransaction()
 
             # Need to know whether the attendee is a location or resource with auto-accept set
-            record = self.directoryService().recordWithGUID(attendee)
-            if record.autoSchedule:
+            record = yield self.directoryService().recordWithUID(attendee)
+            autoScheduleMode = getattr(record, "autoScheduleMode", None)
+            if autoScheduleMode not in (None, AutoScheduleMode.none):
                 # Log details about the event so we can have a human manually process
                 self.fixedAutoAccepts.append(details)
 
@@ -2159,8 +2159,8 @@
             self.txn = None
             uuids = []
             for uuid in sorted(homes):
-                record = self.directoryService().recordWithGUID(uuid)
-                if record is not None and record.recordType in (DirectoryService.recordType_locations, DirectoryService.recordType_resources):
+                record = yield self.directoryService().recordWithUID(uuid)
+                if record is not None and record.recordType in (CalRecordType.location, CalRecordType.resource):
                     uuids.append(uuid)
         else:
             uuids = [self.options["uuid"], ]
@@ -2172,14 +2172,14 @@
             self.total = 0
             count += 1
 
-            record = self.directoryService().recordWithGUID(uuid)
+            record = yield self.directoryService().recordWithUID(uuid)
             if record is None:
                 continue
-            if not record.thisServer() or not record.enabledForCalendaring:
+            if not record.thisServer() or not record.hasCalendars:
                 continue
 
-            rname = record.fullName
-            auto = record.autoSchedule
+            rname = record.displayName
+            autoScheduleMode = getattr(record, "autoSchedule", AutoScheduleMode.none)
 
             if len(uuids) > 1 and not self.options["summary"]:
                 self.output.write("\n\n-----------------------------\n")
@@ -2205,7 +2205,7 @@
             if not self.options["summary"]:
                 self.logResult("UUID to process", uuid)
                 self.logResult("Record name", rname)
-                self.logResult("Auto-schedule", "True" if auto else "False")
+                self.logResult("Auto-schedule-mode", autoScheduleMode.description)
                 self.addSummaryBreak()
                 self.logResult("Number of events to process", self.total)
 
@@ -2216,7 +2216,7 @@
             else:
                 doubled = False
 
-            self.uuid_details.append(UUIDDetails(uuid, rname, auto, doubled))
+            self.uuid_details.append(UUIDDetails(uuid, rname, autoScheduleMode, doubled))
 
             if not self.options["summary"]:
                 self.printSummary()
@@ -2234,7 +2234,7 @@
                 table.addRow((
                     item.uuid,
                     item.rname,
-                    item.auto,
+                    item.autoScheduleMode,
                     item.doubled,
                 ))
                 doubled += 1
@@ -2468,8 +2468,8 @@
             if self.options["verbose"]:
                 self.output.write("%d uuids to check\n" % (len(homes,)))
             for uuid in sorted(homes):
-                record = self.directoryService().recordWithGUID(uuid)
-                if record is not None and record.recordType in (DirectoryService.recordType_locations, DirectoryService.recordType_resources):
+                record = yield self.directoryService().recordWithUID(uuid)
+                if record is not None and record.recordType in (CalRecordType.location, CalRecordType.resource):
                     uuids.append(uuid)
         else:
             uuids = [self.options["uuid"], ]
@@ -2483,13 +2483,13 @@
             self.total = 0
             count += 1
 
-            record = self.directoryService().recordWithGUID(uuid)
+            record = yield self.directoryService().recordWithUID(uuid)
             if record is None:
                 continue
-            if not record.thisServer() or not record.enabledForCalendaring:
+            if not record.thisServer() or not record.hasCalendars:
                 continue
 
-            rname = record.fullName
+            rname = record.displayName
 
             if len(uuids) > 1 and not self.options["summary"]:
                 self.output.write("\n\n-----------------------------\n")
@@ -2602,9 +2602,10 @@
                 if self.options["no-organizer"]:
                     fail = True
             else:
-                principal = self.directoryService().principalForCalendarUserAddress(organizer)
+                principal = yield self.directoryService().recordWithCalendarUserAddress(organizer)
+                # FIXME: Why the mix of records and principals here?
                 if principal is None and organizer.startswith("urn:uuid:"):
-                    principal = self.directoryService().principalCollection.principalForUID(organizer[9:])
+                    principal = yield self.directoryService().principalCollection.principalForUID(organizer[9:])
                 if principal is None:
                     if self.options["invalid-organizer"]:
                         fail = True

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/export.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/export.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/export.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -41,7 +41,7 @@
 
 from twisted.python.text import wordWrap
 from twisted.python.usage import Options, UsageError
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twext.python.log import Logger
 from twistedcaldav.ical import Component
@@ -76,6 +76,7 @@
     )
 )
 
+
 class ExportOptions(Options):
     """
     Command-line options for 'calendarserver_export'
@@ -188,7 +189,7 @@
         Enumerate all calendars based on the directory record and/or calendars
         for this calendar home.
         """
-        uid = self.getHomeUID(exportService)
+        uid = yield self.getHomeUID(exportService)
         home = yield txn.calendarHomeWithUID(uid, True)
         result = []
         if self.collections:
@@ -218,7 +219,7 @@
 
 
     def getHomeUID(self, exportService):
-        return self.uid
+        return succeed(self.uid)
 
 
 
@@ -244,13 +245,17 @@
         self.shortName = shortName
 
 
+    @inlineCallbacks
     def getHomeUID(self, exportService):
         """
         Retrieve the home UID.
         """
         directory = exportService.directoryService()
-        record = directory.recordWithShortName(self.recordType, self.shortName)
-        return record.uid
+        record = yield directory.recordWithShortName(
+            directory.oldNameToRecordType(self.recordType),
+            self.shortName
+        )
+        returnValue(record.uid)
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/gateway.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/gateway.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/gateway.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,31 +19,55 @@
 
 from getopt import getopt, GetoptError
 import os
+from plistlib import readPlistFromString, writePlistToString
 import sys
 import xml
 
-from plistlib import readPlistFromString, writePlistToString
-
-from twisted.internet.defer import inlineCallbacks, succeed
-from twistedcaldav.directory.directory import DirectoryError
-from txdav.xml import element as davxml
-
-from calendarserver.tools.util import (
-    principalForPrincipalID, proxySubprincipal, addProxy, removeProxy,
-    ProxyError, ProxyWarning, autoDisableMemcached
-)
+from calendarserver.tools.cmdline import utilityMain
+from calendarserver.tools.config import WRITABLE_CONFIG_KEYS, setKeyPath, getKeyPath, flattenDictionary, WritableConfig
 from calendarserver.tools.principals import (
-    getProxies, setProxies, updateRecord, attrMap
+    getProxies, setProxies
 )
 from calendarserver.tools.purge import WorkerService, PurgeOldEventsService, DEFAULT_BATCH_SIZE, DEFAULT_RETAIN_DAYS
-from calendarserver.tools.cmdline import utilityMain
-
+from calendarserver.tools.util import (
+    recordForPrincipalID, autoDisableMemcached
+)
 from pycalendar.datetime import DateTime
-
+from twext.who.directory import DirectoryRecord
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
 from twistedcaldav.config import config, ConfigDict
 
-from calendarserver.tools.config import WRITABLE_CONFIG_KEYS, setKeyPath, getKeyPath, flattenDictionary, WritableConfig
+from txdav.who.idirectory import RecordType as CalRecordType
+from twext.who.idirectory import FieldName
+from twisted.python.constants import Names, NamedConstant
+from txdav.who.delegates import (
+    addDelegate, removeDelegate, RecordType as DelegateRecordType
+)
 
+
+attrMap = {
+    'GeneratedUID': {'attr': 'uid', },
+    'RealName': {'attr': 'fullNames', },
+    'RecordName': {'attr': 'shortNames', },
+    'AutoScheduleMode': {'attr': 'autoScheduleMode', },
+    'AutoAcceptGroup': {'attr': 'autoAcceptGroup', },
+
+    # 'Comment': {'extras': True, 'attr': 'comment', },
+    # 'Description': {'extras': True, 'attr': 'description', },
+    # 'Type': {'extras': True, 'attr': 'type', },
+
+    # For "Locations", i.e. scheduled spaces
+    'Capacity': {'attr': 'capacity', },
+    'Floor': {'attr': 'floor', },
+    'AssociatedAddress': {'attr': 'associatedAddress', },
+
+    # For "Addresses", i.e. nonscheduled areas containing Locations
+    'AbbreviatedName': {'attr': 'abbreviatedName', },
+    'StreetAddress': {'attr': 'streetAddress', },
+    'GeographicLocation': {'attr': 'geographicLocation', },
+}
+
+
 def usage(e=None):
 
     name = os.path.basename(sys.argv[0])
@@ -76,9 +100,7 @@
         """
         Create/run a Runner to execute the commands
         """
-        rootResource = self.rootResource()
-        directory = rootResource.getDirectory()
-        runner = Runner(rootResource, directory, self.store, self.commands)
+        runner = Runner(self.store, self.commands)
         if runner.validate():
             yield runner.run()
 
@@ -145,10 +167,9 @@
 
 class Runner(object):
 
-    def __init__(self, root, directory, store, commands, output=None):
-        self.root = root
-        self.dir = directory
+    def __init__(self, store, commands, output=None):
         self.store = store
+        self.dir = store.directoryService()
         self.commands = commands
         if output is None:
             output = sys.stdout
@@ -180,12 +201,13 @@
             pool.ClientEnabled = True
         autoDisableMemcached(config)
 
-        from twistedcaldav.directory import calendaruserproxy
-        if calendaruserproxy.ProxyDBService is not None:
-            # Reset the proxy db memcacher because memcached may have come or
-            # gone since the last time through here.
-            # TODO: figure out a better way to do this
-            calendaruserproxy.ProxyDBService._memcacher._memcacheProtocol = None
+        # FIXME:
+        # from twistedcaldav.directory import calendaruserproxy
+        # if calendaruserproxy.ProxyDBService is not None:
+        #     # Reset the proxy db memcacher because memcached may have come or
+        #     # gone since the last time through here.
+        #     # TODO: figure out a better way to do this
+        #     calendaruserproxy.ProxyDBService._memcacher._memcacheProtocol = None
 
         try:
             for command in self.commands:
@@ -203,224 +225,202 @@
 
     # Locations
 
+    # deferred
     def command_getLocationList(self, command):
-        self.respondWithRecordsOfTypes(self.dir, command, ["locations"])
+        return self.respondWithRecordsOfTypes(self.dir, command, ["locations"])
 
-
     @inlineCallbacks
-    def command_createLocation(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
+    def _saveRecord(self, typeName, recordType, command, oldFields=None):
+        """
+        Save a record using the values in the command plist, starting with
+        any fields in the optional oldFields.
 
-        try:
-            record = (yield updateRecord(True, self.dir, "locations", **kwargs))
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
+        @param typeName: one of "locations", "resources", "addresses"; used
+            to return the appropriate list of records afterwards.
+        @param recordType: the type of record to save
+        @param command: the command containing values
+        @type command: C{dict}
+        @param oldFields: the optional fields to start with, which will be
+            overridden by values from command
+        @type oldFiles: C{dict}
+        """
 
-        readProxies = command.get("ReadProxies", None)
-        writeProxies = command.get("WriteProxies", None)
-        principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
+        if oldFields is None:
+            fields = {
+                FieldName.recordType: recordType
+            }
+            create = True
+        else:
+            fields = oldFields.copy()
+            create = False
 
-        self.respondWithRecordsOfTypes(self.dir, command, ["locations"])
+        for key, info in attrMap.iteritems():
+            if key in command:
+                attrName = info['attr']
+                field = self.dir.fieldName.lookupByName(attrName)
+                valueType = self.dir.fieldName.valueType(field)
+                value = command[key]
 
+                # For backwards compatibility, convert to a list if needed
+                if (
+                    self.dir.fieldName.isMultiValue(field) and
+                    not isinstance(value, list)
+                ):
+                    value = [value]
 
-    @inlineCallbacks
-    def command_getLocationAttributes(self, command):
-        guid = command['GeneratedUID']
-        record = self.dir.recordWithGUID(guid)
-        if record is None:
-            self.respondWithError("Principal not found: %s" % (guid,))
-            return
-        recordDict = recordToDict(record)
-        principal = principalForPrincipalID(guid, directory=self.dir)
-        if principal is None:
-            self.respondWithError("Principal not found: %s" % (guid,))
-            return
-        recordDict['AutoSchedule'] = principal.getAutoSchedule()
-        recordDict['AutoAcceptGroup'] = principal.getAutoAcceptGroup()
-        recordDict['ReadProxies'], recordDict['WriteProxies'] = (yield getProxies(principal,
-            directory=self.dir))
-        self.respond(command, recordDict)
+                if valueType == int:
+                    value = int(value)
+                elif issubclass(valueType, Names):
+                    if value is not None:
+                        value = valueType.lookupByName(value)
+                else:
+                    if isinstance(value, list):
+                        newList = []
+                        for item in value:
+                            if isinstance(item, str):
+                                newList.append(item.decode("utf-8"))
+                            else:
+                                newList.append(item)
+                        value = newList
+                    elif isinstance(value, str):
+                        value = value.decode("utf-8")
 
-    command_getResourceAttributes = command_getLocationAttributes
+                fields[field] = value
 
+        if FieldName.shortNames not in fields:
+            # No short names were provided, so copy from uid
+            fields[FieldName.shortNames] = [fields[FieldName.uid]]
 
-    @inlineCallbacks
-    def command_setLocationAttributes(self, command):
+        record = DirectoryRecord(self.dir, fields)
+        yield self.dir.updateRecords([record], create=create)
 
-        # Set autoSchedule prior to the updateRecord so that the right
-        # value ends up in memcached
-        principal = principalForPrincipalID(command['GeneratedUID'],
-            directory=self.dir)
-        (yield principal.setAutoSchedule(command.get('AutoSchedule', False)))
-        (yield principal.setAutoAcceptGroup(command.get('AutoAcceptGroup', "")))
-
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
-        try:
-            record = (yield updateRecord(False, self.dir, "locations", **kwargs))
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
-
         readProxies = command.get("ReadProxies", None)
+        if readProxies:
+            proxyRecords = []
+            for proxyUID in readProxies:
+                proxyRecord = yield self.dir.recordWithUID(proxyUID)
+                if proxyRecord is not None:
+                    proxyRecords.append(proxyRecord)
+            readProxies = proxyRecords
+
         writeProxies = command.get("WriteProxies", None)
-        principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
+        if writeProxies:
+            proxyRecords = []
+            for proxyUID in writeProxies:
+                proxyRecord = yield self.dir.recordWithUID(proxyUID)
+                if proxyRecord is not None:
+                    proxyRecords.append(proxyRecord)
+            writeProxies = proxyRecords
 
-        yield self.command_getLocationAttributes(command)
+        yield setProxies(record, readProxies, writeProxies)
 
+        yield self.respondWithRecordsOfTypes(self.dir, command, [typeName])
 
-    def command_deleteLocation(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
-        try:
-            self.dir.destroyRecord("locations", **kwargs)
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
-        self.respondWithRecordsOfTypes(self.dir, command, ["locations"])
 
+    def command_createLocation(self, command):
+        return self._saveRecord("locations", CalRecordType.location, command)
 
-    # Resources
 
-    def command_getResourceList(self, command):
-        self.respondWithRecordsOfTypes(self.dir, command, ["resources"])
-
-
-    @inlineCallbacks
     def command_createResource(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
+        return self._saveRecord("resources", CalRecordType.resource, command)
 
-        try:
-            record = (yield updateRecord(True, self.dir, "resources", **kwargs))
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
 
-        readProxies = command.get("ReadProxies", None)
-        writeProxies = command.get("WriteProxies", None)
-        principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
+    def command_createAddress(self, command):
+        return self._saveRecord("addresses", CalRecordType.address, command)
 
-        self.respondWithRecordsOfTypes(self.dir, command, ["resources"])
 
+    @inlineCallbacks
+    def command_setLocationAttributes(self, command):
+        uid = command['GeneratedUID']
+        record = yield self.dir.recordWithUID(uid)
+        yield self._saveRecord(
+            "locations",
+            CalRecordType.location,
+            command,
+            oldFields=record.fields
+        )
 
     @inlineCallbacks
     def command_setResourceAttributes(self, command):
+        uid = command['GeneratedUID']
+        record = yield self.dir.recordWithUID(uid)
+        yield self._saveRecord(
+            "resources",
+            CalRecordType.resource,
+            command,
+            oldFields=record.fields
+        )
 
-        # Set autoSchedule prior to the updateRecord so that the right
-        # value ends up in memcached
-        principal = principalForPrincipalID(command['GeneratedUID'],
-            directory=self.dir)
-        (yield principal.setAutoSchedule(command.get('AutoSchedule', False)))
-        (yield principal.setAutoAcceptGroup(command.get('AutoAcceptGroup', "")))
 
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
-        try:
-            record = (yield updateRecord(False, self.dir, "resources", **kwargs))
-        except DirectoryError, e:
-            self.respondWithError(str(e))
+    @inlineCallbacks
+    def command_setAddressAttributes(self, command):
+        uid = command['GeneratedUID']
+        record = yield self.dir.recordWithUID(uid)
+        yield self._saveRecord(
+            "addresses",
+            CalRecordType.address,
+            command,
+            oldFields=record.fields
+        )
+
+
+    @inlineCallbacks
+    def command_getLocationAttributes(self, command):
+        uid = command['GeneratedUID']
+        record = yield self.dir.recordWithUID(uid)
+        if record is None:
+            self.respondWithError("Principal not found: %s" % (uid,))
             return
+        recordDict = recordToDict(record)
+        # recordDict['AutoSchedule'] = principal.getAutoSchedule()
+        try:
+            recordDict['AutoAcceptGroup'] = record.autoAcceptGroup
+        except AttributeError:
+            pass
 
-        readProxies = command.get("ReadProxies", None)
-        writeProxies = command.get("WriteProxies", None)
-        principal = principalForPrincipalID(record.guid, directory=self.dir)
-        (yield setProxies(self.store, principal, readProxies, writeProxies, directory=self.dir))
+        readProxies, writeProxies = yield getProxies(record)
+        recordDict['ReadProxies'] = [r.uid for r in readProxies]
+        recordDict['WriteProxies'] = [r.uid for r in writeProxies]
+        self.respond(command, recordDict)
 
-        yield self.command_getResourceAttributes(command)
+    command_getResourceAttributes = command_getLocationAttributes
+    command_getAddressAttributes = command_getLocationAttributes
 
 
-    def command_deleteResource(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
-        try:
-            self.dir.destroyRecord("resources", **kwargs)
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
+    # Resources
+
+    def command_getResourceList(self, command):
         self.respondWithRecordsOfTypes(self.dir, command, ["resources"])
 
 
+    # deferred
     def command_getLocationAndResourceList(self, command):
-        self.respondWithRecordsOfTypes(self.dir, command, ["locations", "resources"])
+        return self.respondWithRecordsOfTypes(self.dir, command, ["locations", "resources"])
 
 
     # Addresses
 
     def command_getAddressList(self, command):
-        self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
+        return self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
 
 
     @inlineCallbacks
-    def command_createAddress(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
+    def _delete(self, typeName, command):
+        uid = command['GeneratedUID']
+        yield self.dir.removeRecords([uid])
+        self.respondWithRecordsOfTypes(self.dir, command, [typeName])
 
-        try:
-            yield updateRecord(True, self.dir, "addresses", **kwargs)
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
 
-        self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
+    def command_deleteLocation(self, command):
+        return self._delete("locations", command)
 
 
-    def command_getAddressAttributes(self, command):
-        guid = command['GeneratedUID']
-        record = self.dir.recordWithGUID(guid)
-        if record is None:
-            self.respondWithError("Principal not found: %s" % (guid,))
-            return
-        recordDict = recordToDict(record)
-        self.respond(command, recordDict)
-        return succeed(None)
+    def command_deleteResource(self, command):
+        return self._delete("resources", command)
 
 
-    @inlineCallbacks
-    def command_setAddressAttributes(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
-        try:
-            yield updateRecord(False, self.dir, "addresses", **kwargs)
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
-
-        yield self.command_getAddressAttributes(command)
-
-
     def command_deleteAddress(self, command):
-        kwargs = {}
-        for key, info in attrMap.iteritems():
-            if key in command:
-                kwargs[info['attr']] = command[key]
-        try:
-            self.dir.destroyRecord("addresses", **kwargs)
-        except DirectoryError, e:
-            self.respondWithError(str(e))
-            return
-        self.respondWithRecordsOfTypes(self.dir, command, ["addresses"])
+        return self._delete("addresses", command)
 
 
     # Config
@@ -471,106 +471,74 @@
 
     # Proxies
 
-    @inlineCallbacks
     def command_listWriteProxies(self, command):
-        principal = principalForPrincipalID(command['Principal'], directory=self.dir)
-        if principal is None:
-            self.respondWithError("Principal not found: %s" % (command['Principal'],))
-            return
-        (yield self.respondWithProxies(self.dir, command, principal, "write"))
+        return self._listProxies(command, "write")
 
 
+    def command_listReadProxies(self, command):
+        return self._listProxies(command, "read")
+
     @inlineCallbacks
-    def command_addWriteProxy(self, command):
-        principal = principalForPrincipalID(command['Principal'],
-            directory=self.dir)
-        if principal is None:
+    def _listProxies(self, command, proxyType):
+        record = yield recordForPrincipalID(self.dir, command['Principal'])
+        if record is None:
             self.respondWithError("Principal not found: %s" % (command['Principal'],))
-            return
+            returnValue(None)
+        yield self.respondWithProxies(command, record, proxyType)
 
-        proxy = principalForPrincipalID(command['Proxy'], directory=self.dir)
-        if proxy is None:
-            self.respondWithError("Proxy not found: %s" % (command['Proxy'],))
-            return
-        try:
-            (yield addProxy(self.root, self.dir, self.store, principal, "write", proxy))
-        except ProxyError, e:
-            self.respondWithError(str(e))
-            return
-        except ProxyWarning, e:
-            pass
-        (yield self.respondWithProxies(self.dir, command, principal, "write"))
 
+    def command_addReadProxy(self, command):
+        return self._addProxy(command, "read")
 
+
+    def command_addWriteProxy(self, command):
+        return self._addProxy(command, "write")
+
+
     @inlineCallbacks
-    def command_removeWriteProxy(self, command):
-        principal = principalForPrincipalID(command['Principal'], directory=self.dir)
-        if principal is None:
+    def _addProxy(self, command, proxyType):
+        record = yield recordForPrincipalID(self.dir, command['Principal'])
+        if record is None:
             self.respondWithError("Principal not found: %s" % (command['Principal'],))
-            return
-        proxy = principalForPrincipalID(command['Proxy'], directory=self.dir)
-        if proxy is None:
+            returnValue(None)
+
+        proxyRecord = yield recordForPrincipalID(self.dir, command['Proxy'])
+        if proxyRecord is None:
             self.respondWithError("Proxy not found: %s" % (command['Proxy'],))
-            return
-        try:
-            (yield removeProxy(self.root, self.dir, self.store, principal, proxy, proxyTypes=("write",)))
-        except ProxyError, e:
-            self.respondWithError(str(e))
-            return
-        except ProxyWarning, e:
-            pass
-        (yield self.respondWithProxies(self.dir, command, principal, "write"))
+            returnValue(None)
 
+        txn = self.store.newTransaction()
+        yield addDelegate(txn, record, proxyRecord, (proxyType == "write"))
+        yield txn.commit()
+        yield self.respondWithProxies(command, record, proxyType)
 
-    @inlineCallbacks
-    def command_listReadProxies(self, command):
-        principal = principalForPrincipalID(command['Principal'], directory=self.dir)
-        if principal is None:
-            self.respondWithError("Principal not found: %s" % (command['Principal'],))
-            return
-        (yield self.respondWithProxies(self.dir, command, principal, "read"))
 
+    def command_removeReadProxy(self, command):
+        return self._removeProxy(command, "read")
 
-    @inlineCallbacks
-    def command_addReadProxy(self, command):
-        principal = principalForPrincipalID(command['Principal'], directory=self.dir)
-        if principal is None:
-            self.respondWithError("Principal not found: %s" % (command['Principal'],))
-            return
-        proxy = principalForPrincipalID(command['Proxy'], directory=self.dir)
-        if proxy is None:
-            self.respondWithError("Proxy not found: %s" % (command['Proxy'],))
-            return
-        try:
-            (yield addProxy(self.root, self.dir, self.store, principal, "read", proxy))
-        except ProxyError, e:
-            self.respondWithError(str(e))
-            return
-        except ProxyWarning, e:
-            pass
-        (yield self.respondWithProxies(self.dir, command, principal, "read"))
 
+    def command_removeWriteProxy(self, command):
+        return self._removeProxy(command, "write")
 
+
     @inlineCallbacks
-    def command_removeReadProxy(self, command):
-        principal = principalForPrincipalID(command['Principal'], directory=self.dir)
-        if principal is None:
+    def _removeProxy(self, command, proxyType):
+        record = yield recordForPrincipalID(self.dir, command['Principal'])
+        if record is None:
             self.respondWithError("Principal not found: %s" % (command['Principal'],))
-            return
-        proxy = principalForPrincipalID(command['Proxy'], directory=self.dir)
-        if proxy is None:
+            returnValue(None)
+
+        proxyRecord = yield recordForPrincipalID(self.dir, command['Proxy'])
+        if proxyRecord is None:
             self.respondWithError("Proxy not found: %s" % (command['Proxy'],))
-            return
-        try:
-            (yield removeProxy(self.root, self.dir, self.store, principal, proxy, proxyTypes=("read",)))
-        except ProxyError, e:
-            self.respondWithError(str(e))
-            return
-        except ProxyWarning, e:
-            pass
-        (yield self.respondWithProxies(self.dir, command, principal, "read"))
+            returnValue(None)
 
+        txn = self.store.newTransaction()
+        yield removeDelegate(txn, record, proxyRecord, (proxyType == "write"))
+        yield txn.commit()
+        yield self.respondWithProxies(command, record, proxyType)
 
+
     @inlineCallbacks
     def command_purgeOldEvents(self, command):
         """
@@ -585,40 +553,42 @@
         cutoff.setDateOnly(False)
         cutoff.offsetDay(-retainDays)
         eventCount = (yield PurgeOldEventsService.purgeOldEvents(self.store, cutoff, DEFAULT_BATCH_SIZE))
-        self.respond(command, {'EventsRemoved' : eventCount, "RetainDays" : retainDays})
+        self.respond(command, {'EventsRemoved': eventCount, "RetainDays": retainDays})
 
 
     @inlineCallbacks
-    def respondWithProxies(self, directory, command, principal, proxyType):
+    def respondWithProxies(self, command, record, proxyType):
         proxies = []
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is not None:
-            membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-            if membersProperty.children:
-                for member in membersProperty.children:
-                    proxyPrincipal = principalForPrincipalID(str(member), directory=directory)
-                    proxies.append(proxyPrincipal.record.guid)
+        recordType = {
+            "read": DelegateRecordType.readDelegateGroup,
+            "write": DelegateRecordType.writeDelegateGroup,
+        }[proxyType]
+        proxyGroup = yield self.dir.recordWithShortName(recordType, record.uid)
+        for member in (yield proxyGroup.members()):
+            proxies.append(member.uid)
 
         self.respond(command, {
-            'Principal' : principal.record.guid, 'Proxies' : proxies
+            'Principal': record.uid, 'Proxies': proxies
         })
 
 
+    @inlineCallbacks
     def respondWithRecordsOfTypes(self, directory, command, recordTypes):
         result = []
         for recordType in recordTypes:
-            for record in directory.listRecords(recordType):
+            recordType = directory.oldNameToRecordType(recordType)
+            for record in (yield directory.recordsWithRecordType(recordType)):
                 recordDict = recordToDict(record)
                 result.append(recordDict)
         self.respond(command, result)
 
 
     def respond(self, command, result):
-        self.output.write(writePlistToString({'command' : command['command'], 'result' : result}))
+        self.output.write(writePlistToString({'command': command['command'], 'result': result}))
 
 
     def respondWithError(self, msg, status=1):
-        self.output.write(writePlistToString({'error' : msg, }))
+        self.output.write(writePlistToString({'error': msg, }))
 
 
 
@@ -626,12 +596,17 @@
     recordDict = {}
     for key, info in attrMap.iteritems():
         try:
-            if info.get('extras', False):
-                value = record.extras[info['attr']]
-            else:
-                value = getattr(record, info['attr'])
+            value = record.fields[record.service.fieldName.lookupByName(info['attr'])]
+            if value is None:
+                continue
+            # For backwards compatibility, present fullName/RealName as single
+            # value even though twext.who now has it as multiValue
+            if key == "RealName":
+                value = value[0]
             if isinstance(value, str):
                 value = value.decode("utf-8")
+            elif isinstance(value, NamedConstant):
+                value = value.name
             recordDict[key] = value
         except KeyError:
             pass
@@ -640,7 +615,7 @@
 
 
 def respondWithError(msg, status=1):
-    sys.stdout.write(writePlistToString({'error' : msg, }))
+    sys.stdout.write(writePlistToString({'error': msg, }))
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/migrate.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/migrate.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/migrate.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -33,8 +33,9 @@
 from twistedcaldav.config import ConfigurationError
 from twistedcaldav.upgrade import upgradeData
 
-from calendarserver.tools.util import loadConfig, getDirectory
+from calendarserver.tools.util import loadConfig
 
+
 def usage(e=None):
     if e:
         print(e)
@@ -81,7 +82,6 @@
 
     try:
         config = loadConfig(configFileName)
-        config.directory = getDirectory()
     except ConfigurationError, e:
         sys.stdout.write("%s\n" % (e,))
         sys.exit(1)
@@ -90,7 +90,7 @@
 
     if profiling:
         import cProfile
-        cProfile.runctx("upgradeData(c)", globals(), {"c" : config}, "/tmp/upgrade.prof")
+        cProfile.runctx("upgradeData(c)", globals(), {"c": config}, "/tmp/upgrade.prof")
     else:
         upgradeData(config)
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/principals.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/principals.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/principals.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -17,39 +17,40 @@
 ##
 from __future__ import print_function
 
-import sys
-import os
-import operator
 from getopt import getopt, GetoptError
+import operator
+import os
+import sys
 from uuid import UUID
 
+from calendarserver.tools.cmdline import utilityMain, WorkerService
+from calendarserver.tools.util import (
+    recordForPrincipalID, prettyRecord
+)
+from twext.who.directory import DirectoryRecord
+from twext.who.idirectory import RecordType, InvalidDirectoryRecordError
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
-from txdav.xml import element as davxml
-
-from txdav.xml.base import decodeXMLName, encodeXMLName
-
 from twistedcaldav.config import config
-from twistedcaldav.directory.directory import UnknownRecordTypeError, DirectoryError
-from txdav.who.groups import schedulePolledGroupCachingUpdate
-
-from calendarserver.tools.util import (
-    booleanArgument, proxySubprincipal, action_addProxyPrincipal,
-    principalForPrincipalID, prettyPrincipal, ProxyError,
-    action_removeProxyPrincipal
+from txdav.who.delegates import (
+    addDelegate, removeDelegate, RecordType as DelegateRecordType
 )
-from twistedcaldav.directory.augment import allowedAutoScheduleModes
+from txdav.who.idirectory import AutoScheduleMode
 
-from calendarserver.tools.cmdline import utilityMain, WorkerService
 
+allowedAutoScheduleModes = {
+    "default": None,
+    "none": AutoScheduleMode.none,
+    "accept-always": AutoScheduleMode.accept,
+    "decline-always": AutoScheduleMode.decline,
+    "accept-if-free": AutoScheduleMode.acceptIfFree,
+    "decline-if-busy": AutoScheduleMode.declineIfBusy,
+    "automatic": AutoScheduleMode.acceptIfFreeDeclineIfBusy,
+}
 
+
 def usage(e=None):
     if e:
-        if isinstance(e, UnknownRecordTypeError):
-            print("Valid record types:")
-            for recordType in config.directory.recordTypes():
-                print("    %s" % (recordType,))
-
         print(e)
         print("")
 
@@ -74,20 +75,18 @@
     print("  --search <search-string>: search for matching principals")
     print("  --list-principal-types: list all of the known principal types")
     print("  --list-principals type: list all principals of the given type")
-    print("  --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("  --list-proxy-for: principals this principal is a proxy for")
     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")
     print("  --set-auto-schedule-mode={default|none|accept-always|decline-always|accept-if-free|decline-if-busy|automatic}: set auto-schedule mode")
     print("  --get-auto-schedule-mode: read auto-schedule mode")
     print("  --set-auto-accept-group=principal: set auto-accept-group")
     print("  --get-auto-accept-group: read auto-accept-group")
-    print("  --add {locations|resources|addresses} 'full name' [record name] [GUID]: add a principal")
+    print("  --add {locations|resources|addresses} full-name record-name UID: add a principal")
     print("  --remove: remove a principal")
     print("  --set-geo=url: set the geo: url for an address (e.g. geo:37.331741,-122.030333)")
     print("  --get-geo: get the geo: url for an address")
@@ -102,7 +101,6 @@
         sys.exit(0)
 
 
-
 class PrincipalService(WorkerService):
     """
     Executes principals-related functions in a context which has access to the store
@@ -118,33 +116,10 @@
         resource, directory, store, and whatever has been assigned to "params".
         """
         if self.function is not None:
-            rootResource = self.rootResource()
-            directory = rootResource.getDirectory()
-            yield self.function(rootResource, directory, self.store, *self.params)
+            yield self.function(self.store, *self.params)
 
-attrMap = {
-    'GeneratedUID' : { 'attr' : 'guid', },
-    'RealName' : { 'attr' : 'fullName', },
-    'RecordName' : { 'attr' : 'shortNames', },
-    'AutoSchedule' : { 'attr' : 'autoSchedule', },
-    'AutoAcceptGroup' : { 'attr' : 'autoAcceptGroup', },
 
-    'Comment' : { 'extras' : True, 'attr' : 'comment', },
-    'Description' : { 'extras' : True, 'attr' : 'description', },
-    'Type' : { 'extras' : True, 'attr' : 'type', },
 
-    # For "Locations", i.e. scheduled spaces
-    'Capacity' : { 'extras' : True, 'attr' : 'capacity', },
-    'Floor' : { 'extras' : True, 'attr' : 'floor', },
-    'AssociatedAddress' : { 'extras' : True, 'attr' : 'associatedAddress', },
-
-    # For "Addresses", i.e. nonscheduled areas containing Locations
-    'AbbreviatedName' : { 'extras' : True, 'attr' : 'abbreviatedName', },
-    'StreetAddress' : { 'extras' : True, 'attr' : 'streetAddress', },
-    'Geo' : { 'extras' : True, 'attr' : 'geo', },
-}
-
-
 def main():
     try:
         (optargs, args) = getopt(
@@ -156,15 +131,13 @@
                 "search=",
                 "list-principal-types",
                 "list-principals=",
-                "read-property=",
                 "list-read-proxies",
                 "list-write-proxies",
                 "list-proxies",
+                "list-proxy-for",
                 "add-read-proxy=",
                 "add-write-proxy=",
                 "remove-proxy=",
-                "set-auto-schedule=",
-                "get-auto-schedule",
                 "set-auto-schedule-mode=",
                 "get-auto-schedule-mode",
                 "set-auto-accept-group=",
@@ -193,6 +166,10 @@
     verbose = False
 
     for opt, arg in optargs:
+
+        # Args come in as encoded bytes
+        arg = arg.decode("utf-8")
+
         if opt in ("-h", "--help"):
             usage()
 
@@ -217,13 +194,6 @@
         elif opt in ("", "--search"):
             searchPrincipals = arg
 
-        elif opt in ("", "--read-property"):
-            try:
-                qname = decodeXMLName(arg)
-            except ValueError, e:
-                abort(e)
-            principalActions.append((action_readProperty, qname))
-
         elif opt in ("", "--list-read-proxies"):
             principalActions.append((action_listProxies, "read"))
 
@@ -233,6 +203,9 @@
         elif opt in ("-L", "--list-proxies"):
             principalActions.append((action_listProxies, "read", "write"))
 
+        elif opt in ("--list-proxy-for"):
+            principalActions.append((action_listProxyFor, "read", "write"))
+
         elif opt in ("--add-read-proxy", "--add-write-proxy"):
             if "read" in opt:
                 proxyType = "read"
@@ -240,38 +213,17 @@
                 proxyType = "write"
             else:
                 raise AssertionError("Unknown proxy type")
-
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
-
             principalActions.append((action_addProxy, proxyType, arg))
 
         elif opt in ("", "--remove-proxy"):
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
-
             principalActions.append((action_removeProxy, arg))
 
-        elif opt in ("", "--set-auto-schedule"):
-            try:
-                autoSchedule = booleanArgument(arg)
-            except ValueError, e:
-                abort(e)
-
-            principalActions.append((action_setAutoSchedule, autoSchedule))
-
-        elif opt in ("", "--get-auto-schedule"):
-            principalActions.append((action_getAutoSchedule,))
-
         elif opt in ("", "--set-auto-schedule-mode"):
             try:
                 if arg not in allowedAutoScheduleModes:
-                    raise ValueError("Unknown auto-schedule mode: %s" % (arg,))
-                autoScheduleMode = arg
+                    raise ValueError("Unknown auto-schedule mode: {mode}".format(
+                        mode=arg))
+                autoScheduleMode = allowedAutoScheduleModes[arg]
             except ValueError, e:
                 abort(e)
 
@@ -281,33 +233,28 @@
             principalActions.append((action_getAutoScheduleMode,))
 
         elif opt in ("", "--set-auto-accept-group"):
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
-
             principalActions.append((action_setAutoAcceptGroup, arg))
 
         elif opt in ("", "--get-auto-accept-group"):
             principalActions.append((action_getAutoAcceptGroup,))
 
         elif opt in ("", "--set-geo"):
-            principalActions.append((action_setValue, "Geo", arg))
+            principalActions.append((action_setValue, u"geographicLocation", arg))
 
         elif opt in ("", "--get-geo"):
-            principalActions.append((action_getValue, "Geo"))
+            principalActions.append((action_getValue, u"geographicLocation"))
 
         elif opt in ("", "--set-street-address"):
-            principalActions.append((action_setValue, "StreetAddress", arg))
+            principalActions.append((action_setValue, u"streetAddress", arg))
 
         elif opt in ("", "--get-street-address"):
-            principalActions.append((action_getValue, "StreetAddress"))
+            principalActions.append((action_getValue, u"streetAddress"))
 
         elif opt in ("", "--set-address"):
-            principalActions.append((action_setValue, "AssociatedAddress", arg))
+            principalActions.append((action_setValue, u"associatedAddress", arg))
 
         elif opt in ("", "--get-address"):
-            principalActions.append((action_getValue, "AssociatedAddress"))
+            principalActions.append((action_getValue, u"associatedAddress"))
 
         else:
             raise NotImplementedError(opt)
@@ -325,29 +272,41 @@
     elif addType:
 
         try:
-            addType = matchStrings(addType, ["locations", "resources", "addresses"])
+            addType = matchStrings(
+                addType,
+                [
+                    "locations", "resources", "addresses", "users", "groups"
+                ]
+            )
         except ValueError, e:
             print(e)
             return
 
         try:
-            fullName, shortName, guid = parseCreationArgs(args)
+            fullName, shortName, uid = parseCreationArgs(args)
         except ValueError, e:
             print(e)
             return
 
+        if fullName is not None:
+            fullNames = [fullName]
+        else:
+            fullNames = ()
+
         if shortName is not None:
             shortNames = [shortName]
         else:
             shortNames = ()
 
         function = runAddPrincipal
-        params = (addType, guid, shortNames, fullName)
+        params = (addType, uid, shortNames, fullNames)
 
     elif listPrincipals:
         try:
-            listPrincipals = matchStrings(listPrincipals, ["users", "groups",
-                "locations", "resources", "addresses"])
+            listPrincipals = matchStrings(
+                listPrincipals,
+                ["users", "groups", "locations", "resources", "addresses"]
+            )
         except ValueError, e:
             print(e)
             return
@@ -363,21 +322,12 @@
         params = (searchPrincipals,)
 
     else:
-        #
-        # Do a quick sanity check that arguments look like principal
-        # identifiers.
-        #
         if not args:
             usage("No principals specified.")
 
-        for arg in args:
-            try:
-                principalForPrincipalID(arg, checkOnly=True)
-            except ValueError, e:
-                abort(e)
-
+        unicodeArgs = [a.decode("utf-8") for a in args]
         function = runPrincipalActions
-        params = (args, principalActions)
+        params = (unicodeArgs, principalActions)
 
     PrincipalService.function = function
     PrincipalService.params = params
@@ -385,74 +335,86 @@
 
 
 
-def runListPrincipalTypes(service, rootResource, directory, store):
+def runListPrincipalTypes(service, store):
+    directory = store.directoryService()
     for recordType in directory.recordTypes():
-        print(recordType)
+        print(directory.recordTypeToOldName(recordType))
     return succeed(None)
 
 
 
-def runListPrincipals(service, rootResource, directory, store, listPrincipals):
+ at inlineCallbacks
+def runListPrincipals(service, store, listPrincipals):
+    directory = store.directoryService()
+    recordType = directory.oldNameToRecordType(listPrincipals)
     try:
-        records = list(directory.listRecords(listPrincipals))
+        records = list((yield directory.recordsWithRecordType(recordType)))
         if records:
             printRecordList(records)
         else:
             print("No records of type %s" % (listPrincipals,))
-    except UnknownRecordTypeError, e:
+    except InvalidDirectoryRecordError, e:
         usage(e)
-    return succeed(None)
+    returnValue(None)
 
 
 
 @inlineCallbacks
-def runPrincipalActions(service, rootResource, directory, store, principalIDs,
-    actions):
+def runPrincipalActions(service, store, principalIDs, actions):
+    directory = store.directoryService()
     for principalID in principalIDs:
-        # Resolve the given principal IDs to principals
+        # Resolve the given principal IDs to records
         try:
-            principal = principalForPrincipalID(principalID, directory=directory)
+            record = yield recordForPrincipalID(directory, principalID)
         except ValueError:
-            principal = None
+            record = None
 
-        if principal is None:
+        if record is None:
             sys.stderr.write("Invalid principal ID: %s\n" % (principalID,))
             continue
 
         # Performs requested actions
         for action in actions:
-            (yield action[0](rootResource, directory, store, principal,
-                *action[1:]))
+            (yield action[0](store, record, *action[1:]))
             print("")
 
 
 
 @inlineCallbacks
-def runSearch(service, rootResource, directory, store, searchTerm):
-
+def runSearch(service, store, searchTerm):
+    directory = store.directoryService()
     fields = []
-    for fieldName in ("fullName", "firstName", "lastName", "emailAddresses"):
+    for fieldName in ("fullNames", "emailAddresses"):
         fields.append((fieldName, searchTerm, True, "contains"))
 
     records = list((yield directory.recordsMatchingTokens(searchTerm.strip().split())))
     if records:
-        records.sort(key=operator.attrgetter('fullName'))
-        print("%d matches found:" % (len(records),))
+        records.sort(key=operator.attrgetter('fullNames'))
+        print("{n} matches found:".format(n=len(records)))
         for record in records:
-            print("\n%s (%s)" % (record.fullName,
-                {"users" : "User",
-                 "groups" : "Group",
-                 "locations" : "Place",
-                 "resources" : "Resource",
-                 "addresses" : "Address",
-                }.get(record.recordType),
-            ))
-            print("   GUID: %s" % (record.guid,))
-            print("   Record name(s): %s" % (", ".join(record.shortNames),))
-            if record.authIDs:
-                print("   Auth ID(s): %s" % (", ".join(record.authIDs),))
-            if record.emailAddresses:
-                print("   Email(s): %s" % (", ".join(record.emailAddresses),))
+            print(
+                "\n{d} ({rt})".format(
+                    d=record.displayName,
+                    rt=record.recordType.name
+                )
+            )
+            print("   UID: {u}".format(u=record.uid,))
+            print(
+                "   Record name{plural}: {names}".format(
+                    plural=("s" if len(record.shortNames) > 1 else ""),
+                    names=(", ".join(record.shortNames))
+                )
+            )
+            try:
+                if record.emailAddresses:
+                    print(
+                        "   Email{plural}: {emails}".format(
+                            plural=("s" if len(record.emailAddresses) > 1 else ""),
+                            emails=(", ".join(record.emailAddresses))
+                        )
+                    )
+            except AttributeError:
+                pass
     else:
         print("No matches found")
 
@@ -461,291 +423,346 @@
 
 
 @inlineCallbacks
-def runAddPrincipal(service, rootResource, directory, store, addType, guid,
-    shortNames, fullName):
-    try:
-        yield updateRecord(True, directory, addType, guid=guid,
-            shortNames=shortNames, fullName=fullName)
-        print("Added '%s'" % (fullName,))
-    except DirectoryError, e:
-        print(e)
+def runAddPrincipal(service, store, addType, uid, shortNames, fullNames):
+    directory = store.directoryService()
+    recordType = directory.oldNameToRecordType(addType)
 
+    # See if that UID is in use
+    record = yield directory.recordWithUID(uid)
+    if record is not None:
+        print("UID already in use: {uid}".format(uid=uid))
+        returnValue(None)
 
+    # See if the shortnames are in use
+    for shortName in shortNames:
+        record = yield directory.recordWithShortName(recordType, shortName)
+        if record is not None:
+            print("Record name already in use: {name}".format(name=shortName))
+            returnValue(None)
 
-def action_removePrincipal(rootResource, directory, store, principal):
-    record = principal.record
-    fullName = record.fullName
-    shortName = record.shortNames[0]
-    guid = record.guid
+    fields = {
+        directory.fieldName.recordType: recordType,
+        directory.fieldName.uid: uid,
+        directory.fieldName.shortNames: shortNames,
+        directory.fieldName.fullNames: fullNames,
+    }
+    record = DirectoryRecord(directory, fields)
+    yield record.service.updateRecords([record], create=True)
+    print("Added '{name}'".format(name=fullNames[0]))
 
-    directory.destroyRecord(record.recordType, guid=guid)
-    print("Removed '%s' %s %s" % (fullName, shortName, guid))
 
 
-
 @inlineCallbacks
-def action_readProperty(rootResource, directory, store, resource, qname):
-    property = (yield resource.readProperty(qname, None))
-    print("%r on %s:" % (encodeXMLName(*qname), resource))
-    print("")
-    print(property.toxml())
+def action_removePrincipal(store, record):
+    directory = store.directoryService()
+    fullName = record.displayName
+    shortNames = ",".join(record.shortNames)
 
+    yield directory.removeRecords([record.uid])
+    print(
+        "Removed '{full}' {shorts} {uid}".format(
+            full=fullName, shorts=shortNames, uid=record.uid
+        )
+    )
 
 
+
+
 @inlineCallbacks
-def action_listProxies(rootResource, directory, store, principal, *proxyTypes):
+def action_listProxies(store, record, *proxyTypes):
+    directory = store.directoryService()
     for proxyType in proxyTypes:
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            print("No %s proxies for %s" % (proxyType,
-                prettyPrincipal(principal)))
-            continue
 
-        membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
+        groupRecordType = {
+            "read": directory.recordType.readDelegateGroup,
+            "write": directory.recordType.writeDelegateGroup,
+        }.get(proxyType)
 
-        if membersProperty.children:
+        pseudoGroup = yield directory.recordWithShortName(
+            groupRecordType,
+            record.uid
+        )
+        proxies = yield pseudoGroup.members()
+        if proxies:
             print("%s proxies for %s:" % (
                 {"read": "Read-only", "write": "Read/write"}[proxyType],
-                prettyPrincipal(principal)
+                prettyRecord(record)
             ))
-            records = []
-            for member in membersProperty.children:
-                proxyPrincipal = principalForPrincipalID(str(member),
-                    directory=directory)
-                records.append(proxyPrincipal.record)
-
-            printRecordList(records)
-            print
+            printRecordList(proxies)
+            print("")
         else:
-            print("No %s proxies for %s" % (proxyType,
-                prettyPrincipal(principal)))
+            print("No %s proxies for %s" % (proxyType, prettyRecord(record)))
 
 
+ at inlineCallbacks
+def action_listProxyFor(store, record, *proxyTypes):
+    directory = store.directoryService()
+    for proxyType in proxyTypes:
 
+        groupRecordType = {
+            "read": directory.recordType.readDelegatorGroup,
+            "write": directory.recordType.writeDelegatorGroup,
+        }.get(proxyType)
+
+        pseudoGroup = yield directory.recordWithShortName(
+            groupRecordType,
+            record.uid
+        )
+        proxies = yield pseudoGroup.members()
+        if proxies:
+            print("%s is a %s proxy for:" % (
+                prettyRecord(record),
+                {"read": "Read-only", "write": "Read/write"}[proxyType]
+            ))
+            printRecordList(proxies)
+            print("")
+        else:
+            print(
+                "{r} is not a {t} proxy for anyone".format(
+                    r=prettyRecord(record),
+                    t={"read": "Read-only", "write": "Read/write"}[proxyType]
+                )
+            )
+
+
 @inlineCallbacks
-def action_addProxy(rootResource, directory, store, principal, proxyType, *proxyIDs):
+def _addRemoveProxy(msg, fn, store, record, proxyType, *proxyIDs):
+    directory = store.directoryService()
+    readWrite = (proxyType == "write")
     for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
-        if proxyPrincipal is None:
+        proxyRecord = yield recordForPrincipalID(directory, proxyID)
+        if proxyRecord is None:
             print("Invalid principal ID: %s" % (proxyID,))
         else:
-            (yield action_addProxyPrincipal(rootResource, directory, store,
-                principal, proxyType, proxyPrincipal))
+            txn = store.newTransaction()
+            yield fn(txn, record, proxyRecord, readWrite)
+            yield txn.commit()
+            print(
+                "{msg} {proxy} as a {proxyType} proxy for {record}".format(
+                    msg=msg, proxy=prettyRecord(proxyRecord),
+                    proxyType=proxyType, record=prettyRecord(record)
+                )
+            )
 
 
+ at inlineCallbacks
+def action_addProxy(store, record, proxyType, *proxyIDs):
+    yield _addRemoveProxy("Added", addDelegate, store, record, proxyType, *proxyIDs)
 
+
 @inlineCallbacks
-def setProxies(store, principal, readProxyPrincipals, writeProxyPrincipals, directory=None):
+def action_removeProxy(store, record, *proxyIDs):
+    # Write
+    yield _addRemoveProxy("Removed", removeDelegate, store, record, "write", *proxyIDs)
+    # Read
+    yield _addRemoveProxy("Removed", removeDelegate, store, record, "read", *proxyIDs)
+
+
+
+ at inlineCallbacks
+def setProxies(record, readProxyRecords, writeProxyRecords):
     """
-    Set read/write proxies en masse for a principal
-    @param principal: DirectoryPrincipalResource
-    @param readProxyPrincipals: a list of principal IDs (see principalForPrincipalID)
-    @param writeProxyPrincipals: a list of principal IDs (see principalForPrincipalID)
+    Set read/write proxies en masse for a record
+    @param record: L{IDirectoryRecord}
+    @param readProxyRecords: a list of records
+    @param writeProxyRecords: a list of records
     """
 
     proxyTypes = [
-        ("read", readProxyPrincipals),
-        ("write", writeProxyPrincipals),
+        (DelegateRecordType.readDelegateGroup, readProxyRecords),
+        (DelegateRecordType.writeDelegateGroup, writeProxyRecords),
     ]
-    for proxyType, proxyIDs in proxyTypes:
-        if proxyIDs is None:
+    for recordType, proxyRecords in proxyTypes:
+        if proxyRecords is None:
             continue
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is None:
-            raise ProxyError("Unable to edit %s proxies for %s\n" % (proxyType,
-                prettyPrincipal(principal)))
-        memberURLs = []
-        for proxyID in proxyIDs:
-            proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
-            proxyURL = proxyPrincipal.url()
-            memberURLs.append(davxml.HRef(proxyURL))
-        membersProperty = davxml.GroupMemberSet(*memberURLs)
-        yield subPrincipal.writeProperty(membersProperty, None)
-        if store is not None:
-            # Schedule work the PeerConnectionPool will pick up as overdue
-            yield schedulePolledGroupCachingUpdate(store)
+        proxyGroup = yield record.service.recordWithShortName(
+            recordType, record.uid
+        )
+        yield proxyGroup.setMembers(proxyRecords)
 
+    # if store is not None:
+    #     # Schedule work the PeerConnectionPool will pick up as overdue
+    #     yield schedulePolledGroupCachingUpdate(store)
 
 
+
 @inlineCallbacks
-def getProxies(principal, directory=None):
+def getProxies(record):
     """
-    Returns a tuple containing the GUIDs for read proxies and write proxies
-    of the given principal
+    Returns a tuple containing the records for read proxies and write proxies
+    of the given record
     """
 
-    proxies = {
-        "read" : [],
-        "write" : [],
+    allProxies = {
+        DelegateRecordType.readDelegateGroup: [],
+        DelegateRecordType.writeDelegateGroup: [],
     }
-    for proxyType in proxies.iterkeys():
-        subPrincipal = proxySubprincipal(principal, proxyType)
-        if subPrincipal is not None:
-            membersProperty = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-            if membersProperty.children:
-                for member in membersProperty.children:
-                    proxyPrincipal = principalForPrincipalID(str(member), directory=directory)
-                    proxies[proxyType].append(proxyPrincipal.record.guid)
+    for recordType in allProxies.iterkeys():
+        proxyGroup = yield record.service.recordWithShortName(
+            recordType, record.uid
+        )
+        allProxies[recordType] = yield proxyGroup.members()
 
-    returnValue((proxies['read'], proxies['write']))
+    returnValue(
+        (
+            allProxies[DelegateRecordType.readDelegateGroup],
+            allProxies[DelegateRecordType.writeDelegateGroup]
+        )
+    )
 
 
 
- at inlineCallbacks
-def action_removeProxy(rootResource, directory, store, principal, *proxyIDs, **kwargs):
-    for proxyID in proxyIDs:
-        proxyPrincipal = principalForPrincipalID(proxyID, directory=directory)
-        if proxyPrincipal is None:
-            print("Invalid principal ID: %s" % (proxyID,))
-        else:
-            (yield action_removeProxyPrincipal(rootResource, directory, store,
-                principal, proxyPrincipal, **kwargs))
 
 
+def action_getAutoScheduleMode(store, record):
+    print(
+        "Auto-schedule mode for {record} is {mode}".format(
+            record=prettyRecord(record),
+            mode=(
+                record.autoScheduleMode.description if record.autoScheduleMode
+                else "Default"
+            )
+        )
+    )
 
+
 @inlineCallbacks
-def action_setAutoSchedule(rootResource, directory, store, principal, autoSchedule):
-    if principal.record.recordType == "groups":
-        print("Enabling auto-schedule for %s is not allowed." % (principal,))
+def action_setAutoScheduleMode(store, record, autoScheduleMode):
+    if record.recordType == RecordType.group:
+        print(
+            "Setting auto-schedule-mode for {record} is not allowed.".format(
+                record=prettyRecord(record)
+            )
+        )
 
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
-        print("Enabling auto-schedule for %s is not allowed." % (principal,))
+    elif (
+        record.recordType == RecordType.user and
+        not config.Scheduling.Options.AutoSchedule.AllowUsers
+    ):
+        print(
+            "Setting auto-schedule-mode for {record} is not allowed.".format(
+                record=prettyRecord(record)
+            )
+        )
 
     else:
-        print("Setting auto-schedule to %s for %s" % (
-            {True: "true", False: "false"}[autoSchedule],
-            prettyPrincipal(principal),
-        ))
+        print(
+            "Setting auto-schedule-mode to {mode} for {record}".format(
+                mode=autoScheduleMode.description,
+                record=prettyRecord(record),
+            )
+        )
 
-        (yield updateRecord(False, directory,
-            principal.record.recordType,
-            guid=principal.record.guid,
-            shortNames=principal.record.shortNames,
-            fullName=principal.record.fullName,
-            autoSchedule=autoSchedule,
-            **principal.record.extras
-        ))
+        # Get original fields
+        newFields = record.fields.copy()
 
+        # Set new values
+        newFields[record.service.fieldName.autoScheduleMode] = autoScheduleMode
 
+        updatedRecord = DirectoryRecord(record.service, newFields)
+        yield record.service.updateRecords([updatedRecord], create=False)
 
-def action_getAutoSchedule(rootResource, directory, store, principal):
-    autoSchedule = principal.getAutoSchedule()
-    print("Auto-schedule for %s is %s" % (
-        prettyPrincipal(principal),
-        {True: "true", False: "false"}[autoSchedule],
-    ))
 
-
-
 @inlineCallbacks
-def action_setAutoScheduleMode(rootResource, directory, store, principal, autoScheduleMode):
-    if principal.record.recordType == "groups":
-        print("Setting auto-schedule mode for %s is not allowed." % (principal,))
+def action_setAutoAcceptGroup(store, record, autoAcceptGroup):
+    if record.recordType == RecordType.group:
+        print(
+            "Setting auto-accept-group for {record} is not allowed.".format(
+                record=prettyRecord(record)
+            )
+        )
 
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
-        print("Setting auto-schedule mode for %s is not allowed." % (principal,))
+    elif (
+        record.recordType == RecordType.user and
+        not config.Scheduling.Options.AutoSchedule.AllowUsers
+    ):
+        print(
+            "Setting auto-accept-group for {record} is not allowed.".format(
+                record=prettyRecord(record)
+            )
+        )
 
     else:
-        print("Setting auto-schedule mode to %s for %s" % (
-            autoScheduleMode,
-            prettyPrincipal(principal),
-        ))
+        groupRecord = yield recordForPrincipalID(record.service, autoAcceptGroup)
+        if groupRecord is None or groupRecord.recordType != RecordType.group:
+            print("Invalid principal ID: {id}".format(id=autoAcceptGroup))
+        else:
+            print("Setting auto-accept-group to {group} for {record}".format(
+                group=prettyRecord(groupRecord),
+                record=prettyRecord(record),
+            ))
 
-        (yield updateRecord(False, directory,
-            principal.record.recordType,
-            guid=principal.record.guid,
-            shortNames=principal.record.shortNames,
-            fullName=principal.record.fullName,
-            autoScheduleMode=autoScheduleMode,
-            **principal.record.extras
-        ))
+            # Get original fields
+            newFields = record.fields.copy()
 
+            # Set new values
+            newFields[record.service.fieldName.autoAcceptGroup] = groupRecord.uid
 
+            updatedRecord = DirectoryRecord(record.service, newFields)
+            yield record.service.updateRecords([updatedRecord], create=False)
 
-def action_getAutoScheduleMode(rootResource, directory, store, principal):
-    autoScheduleMode = principal.getAutoScheduleMode()
-    if not autoScheduleMode:
-        autoScheduleMode = "automatic"
-    print("Auto-schedule mode for %s is %s" % (
-        prettyPrincipal(principal),
-        autoScheduleMode,
-    ))
 
 
-
 @inlineCallbacks
-def action_setAutoAcceptGroup(rootResource, directory, store, principal, autoAcceptGroup):
-    if principal.record.recordType == "groups":
-        print("Setting auto-accept-group for %s is not allowed." % (principal,))
-
-    elif principal.record.recordType == "users" and not config.Scheduling.Options.AutoSchedule.AllowUsers:
-        print("Setting auto-accept-group for %s is not allowed." % (principal,))
-
-    else:
-        groupPrincipal = principalForPrincipalID(autoAcceptGroup, directory=directory)
-        if groupPrincipal is None or groupPrincipal.record.recordType != "groups":
-            print("Invalid principal ID: %s" % (autoAcceptGroup,))
+def action_getAutoAcceptGroup(store, record):
+    if record.autoAcceptGroup:
+        groupRecord = yield record.service.recordWithUID(
+            record.autoAcceptGroup
+        )
+        if groupRecord is not None:
+            print(
+                "Auto-accept-group for {record} is {group}".format(
+                    record=prettyRecord(record),
+                    group=prettyRecord(groupRecord),
+                )
+            )
         else:
-            print("Setting auto-accept-group to %s for %s" % (
-                prettyPrincipal(groupPrincipal),
-                prettyPrincipal(principal),
-            ))
-
-            (yield updateRecord(False, directory,
-                principal.record.recordType,
-                guid=principal.record.guid,
-                shortNames=principal.record.shortNames,
-                fullName=principal.record.fullName,
-                autoAcceptGroup=groupPrincipal.record.guid,
-                **principal.record.extras
-            ))
-
-
-
-def action_getAutoAcceptGroup(rootResource, directory, store, principal):
-    autoAcceptGroup = principal.getAutoAcceptGroup()
-    if autoAcceptGroup:
-        record = directory.recordWithGUID(autoAcceptGroup)
-        if record is not None:
-            groupPrincipal = directory.principalCollection.principalForUID(record.uid)
-            if groupPrincipal is not None:
-                print("Auto-accept-group for %s is %s" % (
-                    prettyPrincipal(principal),
-                    prettyPrincipal(groupPrincipal),
-                ))
-                return
-        print("Invalid auto-accept-group assigned: %s" % (autoAcceptGroup,))
+            print(
+                "Invalid auto-accept-group assigned: {uid}".format(
+                    uid=record.autoAcceptGroup
+                )
+            )
     else:
-        print("No auto-accept-group assigned to %s" % (prettyPrincipal(principal),))
+        print(
+            "No auto-accept-group assigned to {record}".format(
+                record=prettyRecord(record)
+            )
+        )
 
 
-
 @inlineCallbacks
-def action_setValue(rootResource, directory, store, principal, name, value):
-    print("Setting %s to %s for %s" % (
-        name, value, prettyPrincipal(principal),
-    ))
+def action_setValue(store, record, name, value):
+    print(
+        "Setting {name} to {value} for {record}".format(
+            name=name, value=value, record=prettyRecord(record),
+        )
+    )
+    # Get original fields
+    newFields = record.fields.copy()
 
-    principal.record.extras[attrMap[name]["attr"]] = value
-    (yield updateRecord(False, directory,
-        principal.record.recordType,
-        guid=principal.record.guid,
-        shortNames=principal.record.shortNames,
-        fullName=principal.record.fullName,
-        **principal.record.extras
-    ))
+    # Set new value
+    newFields[record.service.fieldName.lookupByName(name)] = value
 
+    updatedRecord = DirectoryRecord(record.service, newFields)
+    yield record.service.updateRecords([updatedRecord], create=False)
 
 
-def action_getValue(rootResource, directory, store, principal, name):
-    print("%s for %s is %s" % (
-        name,
-        prettyPrincipal(principal),
-        principal.record.extras[attrMap[name]["attr"]]
-    ))
+def action_getValue(store, record, name):
+    try:
+        value = record.fields[record.service.fieldName.lookupByName(name)]
+        print(
+            "{name} for {record} is {value}".format(
+                name=name, record=prettyRecord(record), value=value
+            )
+        )
+    except KeyError:
+        print(
+            "{name} is not set for {record}".format(
+                name=name, record=prettyRecord(record),
+            )
+        )
 
 
-
 def abort(msg, status=1):
     sys.stdout.write("%s\n" % (msg,))
     try:
@@ -758,29 +775,23 @@
 
 def parseCreationArgs(args):
     """
-    Look at the command line arguments for --add, and figure out which
-    one is the shortName and which one is the guid by attempting to make a
-    UUID object out of them.
+    Look at the command line arguments for --add, and simply assume the first
+    is full name, the second is short name, and the third is uid.  We can make
+    this fancier later.
     """
 
-    fullName = args[0]
-    shortName = None
-    guid = None
-    for arg in args[1:]:
-        if isUUID(arg):
-            if guid is not None:
-                # Both the 2nd and 3rd args are UUIDs.  The first one
-                # should be used for shortName.
-                shortName = guid
-            guid = arg
-        else:
-            shortName = arg
+    if len(args) != 3:
+        print(
+            "When adding a principal, you must provide full-name, record-name, "
+            "and UID"
+        )
+        sys.exit(64)
 
-    if len(args) == 3 and guid is None:
-        # both shortName and guid were specified but neither was a UUID
-        raise ValueError("Invalid value for guid")
+    fullName = args[0].decode("utf-8")
+    shortName = args[1].decode("utf-8")
+    uid = args[2].decode("utf-8")
 
-    return fullName, shortName, guid
+    return fullName, shortName, uid
 
 
 
@@ -803,95 +814,20 @@
 
 
 def printRecordList(records):
-    results = [(record.fullName, record.shortNames[0], record.guid)
-        for record in records]
+    results = [
+        (record.displayName, record.recordType.name, record.uid, record.shortNames)
+        for record in records
+    ]
     results.sort()
-    format = "%-22s %-17s %s"
-    print(format % ("Full name", "Record name", "UUID"))
-    print(format % ("---------", "-----------", "----"))
-    for fullName, shortName, guid in results:
-        print(format % (fullName, shortName, guid))
+    format = "%-22s %-10s %-20s %s"
+    print(format % ("Full name", "Type", "UID", "Short names"))
+    print(format % ("---------", "----", "---", "-----------"))
+    for fullName, recordType, uid, shortNames in results:
+        print(format % (fullName, recordType, uid, u", ".join(shortNames)))
 
 
 
- at inlineCallbacks
-def updateRecord(create, directory, recordType, **kwargs):
-    """
-    Create/update a record, including the extra work required to set the
-    autoSchedule bit in the augment record.
 
-    If C{create} is true, the record is created, otherwise update the record
-    matching the guid in kwargs.
-    """
 
-    assignAutoSchedule = False
-    if "autoSchedule" in kwargs:
-        assignAutoSchedule = True
-        autoSchedule = kwargs["autoSchedule"]
-        del kwargs["autoSchedule"]
-    elif create:
-        assignAutoSchedule = True
-        autoSchedule = recordType in ("locations", "resources")
-
-    assignAutoScheduleMode = False
-    if "autoScheduleMode" in kwargs:
-        assignAutoScheduleMode = True
-        autoScheduleMode = kwargs["autoScheduleMode"]
-        del kwargs["autoScheduleMode"]
-    elif create:
-        assignAutoScheduleMode = True
-        autoScheduleMode = None
-
-    assignAutoAcceptGroup = False
-    if "autoAcceptGroup" in kwargs:
-        assignAutoAcceptGroup = True
-        autoAcceptGroup = kwargs["autoAcceptGroup"]
-        del kwargs["autoAcceptGroup"]
-    elif create:
-        assignAutoAcceptGroup = True
-        autoAcceptGroup = None
-
-    for key, value in kwargs.items():
-        if isinstance(value, unicode):
-            kwargs[key] = value.encode("utf-8")
-        elif isinstance(value, list):
-            newValue = [v.encode("utf-8") for v in value]
-            kwargs[key] = newValue
-
-    if create:
-        record = directory.createRecord(recordType, **kwargs)
-        kwargs['guid'] = record.guid
-    else:
-        try:
-            record = directory.updateRecord(recordType, **kwargs)
-        except NotImplementedError:
-            # Updating of directory information is not supported by underlying
-            # directory implementation, but allow augment information to be
-            # updated
-            record = directory.recordWithGUID(kwargs["guid"])
-            pass
-
-    augmentService = directory.serviceForRecordType(recordType).augmentService
-    augmentRecord = (yield augmentService.getAugmentRecord(kwargs['guid'], recordType))
-
-    if assignAutoSchedule:
-        augmentRecord.autoSchedule = autoSchedule
-    if assignAutoScheduleMode:
-        augmentRecord.autoScheduleMode = autoScheduleMode
-    if assignAutoAcceptGroup:
-        augmentRecord.autoAcceptGroup = autoAcceptGroup
-    (yield augmentService.addAugmentRecords([augmentRecord]))
-    try:
-        directory.updateRecord(recordType, **kwargs)
-    except NotImplementedError:
-        # Updating of directory information is not supported by underlying
-        # directory implementation, but allow augment information to be
-        # updated
-        pass
-
-    returnValue(record)
-
-
-
 if __name__ == "__main__":
     main()

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/purge.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/purge.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,7 +19,6 @@
 
 from calendarserver.tools import tables
 from calendarserver.tools.cmdline import utilityMain, WorkerService
-from calendarserver.tools.util import removeProxy
 
 from getopt import getopt, GetoptError
 
@@ -30,10 +29,9 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav import caldavxml
-from twistedcaldav.directory.directory import DirectoryRecord
+# from twistedcaldav.directory.directory import DirectoryRecord
 
 from txdav.caldav.datastore.query.filter import Filter
-from txdav.xml import element as davxml
 
 
 import collections
@@ -170,7 +168,7 @@
         service.batchSize = batchSize
         service.dryrun = dryrun
         service.verbose = verbose
-        result = (yield service.doWork())
+        result = yield service.doWork()
         returnValue(result)
 
 
@@ -181,7 +179,7 @@
             if self.verbose:
                 print("(Dry run) Searching for old events...")
             txn = self.store.newTransaction(label="Find old events")
-            oldEvents = (yield txn.eventsOlderThan(self.cutoff))
+            oldEvents = yield txn.eventsOlderThan(self.cutoff)
             eventCount = len(oldEvents)
             if self.verbose:
                 if eventCount == 0:
@@ -199,8 +197,8 @@
         totalRemoved = 0
         while numEventsRemoved:
             txn = self.store.newTransaction(label="Remove old events")
-            numEventsRemoved = (yield txn.removeOldEvents(self.cutoff, batchSize=self.batchSize))
-            (yield txn.commit())
+            numEventsRemoved = yield txn.removeOldEvents(self.cutoff, batchSize=self.batchSize)
+            yield txn.commit()
             if numEventsRemoved:
                 totalRemoved += numEventsRemoved
                 if self.verbose:
@@ -360,7 +358,7 @@
         service.batchSize = limit
         service.dryrun = dryrun
         service.verbose = verbose
-        result = (yield service.doWork())
+        result = yield service.doWork()
         returnValue(result)
 
 
@@ -368,20 +366,20 @@
     def doWork(self):
 
         if self.dryrun:
-            orphans = (yield self._orphansDryRun())
+            orphans = yield self._orphansDryRun()
             if self.cutoff is not None:
-                dropbox = (yield self._dropboxDryRun())
-                managed = (yield self._managedDryRun())
+                dropbox = yield self._dropboxDryRun()
+                managed = yield self._managedDryRun()
             else:
                 dropbox = ()
                 managed = ()
 
             returnValue(self._dryRunSummary(orphans, dropbox, managed))
         else:
-            total = (yield self._orphansPurge())
+            total = yield self._orphansPurge()
             if self.cutoff is not None:
-                total += (yield self._dropboxPurge())
-                total += (yield self._managedPurge())
+                total += yield self._dropboxPurge()
+                total += yield self._managedPurge()
             returnValue(total)
 
 
@@ -391,7 +389,7 @@
         if self.verbose:
             print("(Dry run) Searching for orphaned attachments...")
         txn = self.store.newTransaction(label="Find orphaned attachments")
-        orphans = (yield txn.orphanedAttachments(self.uuid))
+        orphans = yield txn.orphanedAttachments(self.uuid)
         returnValue(orphans)
 
 
@@ -401,7 +399,7 @@
         if self.verbose:
             print("(Dry run) Searching for old dropbox attachments...")
         txn = self.store.newTransaction(label="Find old dropbox attachments")
-        cutoffs = (yield txn.oldDropboxAttachments(self.cutoff, self.uuid))
+        cutoffs = yield txn.oldDropboxAttachments(self.cutoff, self.uuid)
         yield txn.commit()
 
         returnValue(cutoffs)
@@ -413,7 +411,7 @@
         if self.verbose:
             print("(Dry run) Searching for old managed attachments...")
         txn = self.store.newTransaction(label="Find old managed attachments")
-        cutoffs = (yield txn.oldManagedAttachments(self.cutoff, self.uuid))
+        cutoffs = yield txn.oldManagedAttachments(self.cutoff, self.uuid)
         yield txn.commit()
 
         returnValue(cutoffs)
@@ -495,7 +493,7 @@
         totalRemoved = 0
         while numOrphansRemoved:
             txn = self.store.newTransaction(label="Remove orphaned attachments")
-            numOrphansRemoved = (yield txn.removeOrphanedAttachments(self.uuid, batchSize=self.batchSize))
+            numOrphansRemoved = yield txn.removeOrphanedAttachments(self.uuid, batchSize=self.batchSize)
             yield txn.commit()
             if numOrphansRemoved:
                 totalRemoved += numOrphansRemoved
@@ -526,7 +524,7 @@
         totalRemoved = 0
         while numOldRemoved:
             txn = self.store.newTransaction(label="Remove old dropbox attachments")
-            numOldRemoved = (yield txn.removeOldDropboxAttachments(self.cutoff, self.uuid, batchSize=self.batchSize))
+            numOldRemoved = yield txn.removeOldDropboxAttachments(self.cutoff, self.uuid, batchSize=self.batchSize)
             yield txn.commit()
             if numOldRemoved:
                 totalRemoved += numOldRemoved
@@ -557,7 +555,7 @@
         totalRemoved = 0
         while numOldRemoved:
             txn = self.store.newTransaction(label="Remove old managed attachments")
-            numOldRemoved = (yield txn.removeOldManagedAttachments(self.cutoff, self.uuid, batchSize=self.batchSize))
+            numOldRemoved = yield txn.removeOldManagedAttachments(self.cutoff, self.uuid, batchSize=self.batchSize)
             yield txn.commit()
             if numOldRemoved:
                 totalRemoved += numOldRemoved
@@ -697,7 +695,7 @@
         service.doimplicit = doimplicit
         service.proxies = proxies
         service.when = when
-        result = (yield service.doWork())
+        result = yield service.doWork()
         returnValue(result)
 
 
@@ -711,10 +709,8 @@
 
         total = 0
 
-        allAssignments = {}
-
         for uid in self.uids:
-            count, allAssignments[uid] = (yield self._purgeUID(uid))
+            count = yield self._purgeUID(uid)
             total += count
 
         if self.verbose:
@@ -724,7 +720,7 @@
             else:
                 print("Modified or deleted %s" % (amount,))
 
-        returnValue((total, allAssignments,))
+        returnValue(total)
 
 
     @inlineCallbacks
@@ -734,30 +730,26 @@
             self.when = DateTime.getNowUTC()
 
         # Does the record exist?
-        record = self.directory.recordWithUID(uid)
-        if record is None:
+        record = yield self.directory.recordWithUID(uid)
+        # if record is None:
             # The user has already been removed from the directory service.  We
             # need to fashion a temporary, fake record
 
             # FIXME: probably want a more elegant way to accomplish this,
             # since it requires the aggregate directory to examine these first:
-            record = DirectoryRecord(self.directory, "users", uid, shortNames=(uid,), enabledForCalendaring=True)
-            self.directory._tmpRecords["shortNames"][uid] = record
-            self.directory._tmpRecords["uids"][uid] = record
+            # record = DirectoryRecord(self.directory, "users", uid, shortNames=(uid,), enabledForCalendaring=True)
+            # self.directory._tmpRecords["shortNames"][uid] = record
+            # self.directory._tmpRecords["uids"][uid] = record
 
         # Override augments settings for this record
-        record.enabled = True
-        record.enabledForCalendaring = True
-        record.enabledForAddressBooks = True
+        record.hasCalendars = True
+        record.hasContacts = True
 
-        cua = "urn:uuid:%s" % (uid,)
+        cua = record.canonicalCalendarUserAddress()
 
-        principalCollection = self.directory.principalCollection
-        principal = principalCollection.principalForRecord(record)
-
         # See if calendar home is provisioned
         txn = self.store.newTransaction()
-        storeCalHome = (yield txn.calendarHomeWithUID(uid))
+        storeCalHome = yield txn.calendarHomeWithUID(uid)
         calHomeProvisioned = storeCalHome is not None
 
         # If in "completely" mode, unshare collections, remove notifications
@@ -767,24 +759,23 @@
         yield txn.commit()
 
         count = 0
-        assignments = []
 
         if calHomeProvisioned:
-            count = (yield self._cancelEvents(txn, uid, cua))
+            count = yield self._cancelEvents(txn, uid, cua)
 
         # Remove empty calendar collections (and calendar home if no more
         # calendars)
         yield self._removeCalendarHome(uid)
 
         # Remove VCards
-        count += (yield self._removeAddressbookHome(uid))
+        count += yield self._removeAddressbookHome(uid)
 
         if self.proxies and not self.dryrun:
             if self.verbose:
                 print("Deleting any proxy assignments")
-            assignments = (yield self._purgeProxyAssignments(principal))
+            yield self._purgeProxyAssignments(self.store, record)
 
-        returnValue((count, assignments))
+        returnValue(count)
 
 
     @inlineCallbacks
@@ -799,13 +790,13 @@
                 else:
                     print("Unsharing: %s" % (child.name(),))
             if not self.dryrun:
-                (yield child.unshare())
+                yield child.unshare()
 
         if not self.dryrun:
-            (yield storeCalHome.removeUnacceptedShares())
-            notificationHome = (yield txn.notificationsWithUID(storeCalHome.uid()))
+            yield storeCalHome.removeUnacceptedShares()
+            notificationHome = yield txn.notificationsWithUID(storeCalHome.uid())
             if notificationHome is not None:
-                (yield notificationHome.remove())
+                yield notificationHome.remove()
 
 
     @inlineCallbacks
@@ -826,15 +817,15 @@
 
         count = 0
         txn = self.store.newTransaction()
-        storeCalHome = (yield txn.calendarHomeWithUID(uid))
-        calendarNames = (yield storeCalHome.listCalendars())
+        storeCalHome = yield txn.calendarHomeWithUID(uid)
+        calendarNames = yield storeCalHome.listCalendars()
         yield txn.commit()
 
         for calendarName in calendarNames:
 
             txn = self.store.newTransaction(authz_uid=uid)
-            storeCalHome = (yield txn.calendarHomeWithUID(uid))
-            calendar = (yield storeCalHome.calendarWithName(calendarName))
+            storeCalHome = yield txn.calendarHomeWithUID(uid)
+            calendar = yield storeCalHome.calendarWithName(calendarName)
             childNames = []
 
             if self.completely:
@@ -850,17 +841,17 @@
             for childName in childNames:
 
                 txn = self.store.newTransaction(authz_uid=uid)
-                storeCalHome = (yield txn.calendarHomeWithUID(uid))
-                calendar = (yield storeCalHome.calendarWithName(calendarName))
+                storeCalHome = yield txn.calendarHomeWithUID(uid)
+                calendar = yield storeCalHome.calendarWithName(calendarName)
 
                 try:
-                    childResource = (yield calendar.calendarObjectWithName(childName))
+                    childResource = yield calendar.calendarObjectWithName(childName)
 
                     # Always delete inbox items
                     if self.completely or calendar.isInbox():
                         action = self.CANCELEVENT_SHOULD_DELETE
                     else:
-                        event = (yield childResource.componentForUser())
+                        event = yield childResource.componentForUser()
                         action = self._cancelEvent(event, self.when, cua)
 
                     uri = "/calendars/__uids__/%s/%s/%s" % (storeCalHome.uid(), calendar.name(), childName)
@@ -921,7 +912,7 @@
 
             # Remove empty calendar collections (and calendar home if no more
             # calendars)
-            storeCalHome = (yield txn.calendarHomeWithUID(uid))
+            storeCalHome = yield txn.calendarHomeWithUID(uid)
             if storeCalHome is not None:
                 calendars = list((yield storeCalHome.calendars()))
                 remainingCalendars = len(calendars)
@@ -947,7 +938,7 @@
                         else:
                             print("Deleting calendar home")
                     if not self.dryrun:
-                        (yield storeCalHome.remove())
+                        yield storeCalHome.remove()
 
             # Commit
             yield txn.commit()
@@ -966,7 +957,7 @@
 
         try:
             # Remove VCards
-            storeAbHome = (yield txn.addressbookHomeWithUID(uid))
+            storeAbHome = yield txn.addressbookHomeWithUID(uid)
             if storeAbHome is not None:
                 for abColl in list((yield storeAbHome.addressbooks())):
                     for card in list((yield abColl.addressbookObjects())):
@@ -978,7 +969,7 @@
                             else:
                                 print("Deleting: %s" % (uri,))
                         if not self.dryrun:
-                            (yield card.remove())
+                            yield card.remove()
                         count += 1
                     abName = abColl.name()
                     if self.verbose:
@@ -988,10 +979,14 @@
                             print("Deleting addressbook: %s" % (abName,))
                     if not self.dryrun:
                         # Also remove the addressbook collection itself
+<<<<<<< .working
                         if abColl.owned():
                             yield storeAbHome.removeChildWithName(abName)
                         else:
                             yield abColl.unshare()
+=======
+                        yield storeAbHome.removeChildWithName(abColl.name())
+>>>>>>> .merge-right.r13157
 
                 if self.verbose:
                     if self.dryrun:
@@ -999,7 +994,7 @@
                     else:
                         print("Deleting addressbook home")
                 if not self.dryrun:
-                    (yield storeAbHome.remove())
+                    yield storeAbHome.remove()
 
             # Commit
             yield txn.commit()
@@ -1113,22 +1108,10 @@
 
 
     @inlineCallbacks
-    def _purgeProxyAssignments(self, principal):
+    def _purgeProxyAssignments(self, store, record):
 
-        assignments = []
-
-        for proxyType in ("read", "write"):
-
-            proxyFor = (yield principal.proxyFor(proxyType == "write"))
-            for other in proxyFor:
-                assignments.append((principal.record.uid, proxyType, other.record.uid))
-                (yield removeProxy(self.root, self.directory, self.store, other, principal))
-
-            subPrincipal = principal.getChild("calendar-proxy-" + proxyType)
-            proxies = (yield subPrincipal.readProperty(davxml.GroupMemberSet, None))
-            for other in proxies.children:
-                assignments.append((str(other).split("/")[3], proxyType, principal.record.uid))
-
-            (yield subPrincipal.writeProperty(davxml.GroupMemberSet(), None))
-
-        returnValue(assignments)
+        txn = store.newTransaction()
+        for readWrite in (True, False):
+            yield txn.removeDelegates(record.uid, readWrite)
+            yield txn.removeDelegateGroupss(record.uid, readWrite)
+        yield txn.commit()

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/push.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/push.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/push.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,6 +20,7 @@
 from argparse import ArgumentParser
 from twext.python.log import Logger
 from twisted.internet.defer import inlineCallbacks
+from twext.who.idirectory import RecordType
 import time
 
 log = Logger()
@@ -59,7 +60,7 @@
 def displayAPNSubscriptions(store, directory, root, users):
     for user in users:
         print
-        record = directory.recordWithShortName("users", user)
+        record = yield directory.recordWithShortName(RecordType.user, user)
         if record is not None:
             print("User %s (%s)..." % (user, record.uid))
             txn = store.newTransaction(label="Display APN Subscriptions")
@@ -81,7 +82,7 @@
                     else:
                         uid = path
                         collection = None
-                    record = directory.recordWithUID(uid)
+                    record = yield directory.recordWithUID(uid)
                     user = record.shortNames[0]
                     if collection:
                         print("...is subscribed to a share from %s's %s home" % (user, resource),)

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/resources.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/resources.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/resources.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -15,38 +15,36 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+
 from __future__ import print_function
 
+__all__ = [
+    "migrateResources",
+]
+
+from getopt import getopt, GetoptError
+from grp import getgrnam
 import os
+from pwd import getpwnam
 import sys
-from grp import getgrnam
-from pwd import getpwnam
-from getopt import getopt, GetoptError
 
+from calendarserver.tools.util import (
+    loadConfig, setupMemcached, checkDirectory
+)
+from twext.python.log import Logger, StandardIOObserver
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks
 from twisted.python.util import switchUID
-
-from twext.python.log import Logger, StandardIOObserver
-
 from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
 from twistedcaldav.directory.directory import DirectoryService, DirectoryError
 from twistedcaldav.directory.xmlfile import XMLDirectoryService
+from txdav.who.util import directoryFromConfig
 
-from calendarserver.platform.darwin.od import dsattributes
-from calendarserver.tools.util import loadConfig, getDirectory, setupMemcached, checkDirectory
-
 log = Logger()
 
 
 
-__all__ = [
-    "migrateResources",
-]
-
-
-
 def usage():
 
     name = os.path.basename(sys.argv[0])
@@ -141,28 +139,37 @@
         os.umask(config.umask)
 
         # Configure memcached client settings prior to setting up resource
-        # hierarchy (in getDirectory)
+        # hierarchy
         setupMemcached(config)
 
         try:
-            config.directory = getDirectory()
+            config.directory = directoryFromConfig(config)
         except DirectoryError, e:
             abort(e)
 
     except ConfigurationError, e:
         abort(e)
 
+    # FIXME: this all has to change:
     # Find the opendirectory service
     userService = config.directory.serviceForRecordType("users")
     resourceService = config.directory.serviceForRecordType("resources")
-    if (not isinstance(userService, OpenDirectoryService) or
-        not isinstance(resourceService, XMLDirectoryService)):
-        abort("This script only migrates resources and locations from OpenDirectory to XML; this calendar server does not have such a configuration.")
+    if (
+        not isinstance(userService, OpenDirectoryService) or
+        not isinstance(resourceService, XMLDirectoryService)
+    ):
+        abort(
+            "This script only migrates resources and locations from "
+            "OpenDirectory to XML; this calendar server does not have such a "
+            "configuration."
+        )
 
     #
     # Start the reactor
     #
-    reactor.callLater(0, migrate, userService, resourceService, verbose=verbose)
+    reactor.callLater(
+        0, migrate, userService, resourceService, verbose=verbose
+    )
     reactor.run()
 
 
@@ -186,8 +193,8 @@
     """
 
     attrs = [
-        dsattributes.kDS1AttrGeneratedUID,
-        dsattributes.kDS1AttrDistinguishedName,
+        "dsAttrTypeStandard:GeneratedUID",
+        "dsAttrTypeStandard:RealName",
     ]
 
     if verbose:
@@ -207,24 +214,26 @@
 
 
 @inlineCallbacks
-def migrateResources(sourceService, destService, autoSchedules=None,
-    queryMethod=queryForType, verbose=False):
+def migrateResources(
+    sourceService, destService, autoSchedules=None,
+    queryMethod=queryForType, verbose=False
+):
 
     directoryRecords = []
     augmentRecords = []
 
     for recordTypeOD, recordType in (
-        (dsattributes.kDSStdRecordTypeResources, DirectoryService.recordType_resources),
-        (dsattributes.kDSStdRecordTypePlaces, DirectoryService.recordType_locations),
+        ("dsRecTypeStandard:Resources", DirectoryService.recordType_resources),
+        ("dsRecTypeStandard:Places", DirectoryService.recordType_locations),
     ):
         data = queryMethod(sourceService, recordTypeOD, verbose=verbose)
         for recordName, val in data:
-            guid = val.get(dsattributes.kDS1AttrGeneratedUID, None)
-            fullName = val.get(dsattributes.kDS1AttrDistinguishedName, None)
+            guid = val.get("dsAttrTypeStandard:GeneratedUID", None)
+            fullName = val.get("dsAttrTypeStandard:RealName", None)
             if guid and fullName:
                 if not recordName:
                     recordName = guid
-                record = destService.recordWithGUID(guid)
+                record = yield destService.recordWithGUID(guid)
                 if record is None:
                     if verbose:
                         print("Migrating %s (%s)" % (fullName, recordType))
@@ -233,23 +242,29 @@
                         autoSchedule = autoSchedules.get(guid, 1)
                     else:
                         autoSchedule = True
-                    augmentRecord = (yield destService.augmentService.getAugmentRecord(guid, recordType))
-                    augmentRecord.autoSchedule = autoSchedule
-                    augmentRecords.append(augmentRecord)
-
-                    directoryRecords.append(
-                        (recordType,
-                            {
-                                "guid" : guid,
-                                "shortNames" : [recordName],
-                                "fullName" : fullName,
-                            }
+                    augmentRecord = (
+                        yield destService.augmentService.getAugmentRecord(
+                            guid, recordType
                         )
                     )
+                    if autoSchedule:
+                        augmentRecord.autoScheduleMode = "automatic"
+                    else:
+                        augmentRecord.autoScheduleMode = "none"
+                    augmentRecords.append(augmentRecord)
 
+                    directoryRecords.append((
+                        recordType,
+                        {
+                            "guid": guid,
+                            "shortNames": [recordName],
+                            "fullName": fullName,
+                        }
+                    ))
+
     destService.createRecords(directoryRecords)
 
-    (yield destService.augmentService.addAugmentRecords(augmentRecords))
+    yield destService.augmentService.addAugmentRecords(augmentRecords)
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/directory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/directory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/directory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -83,16 +83,22 @@
     add("First Name", record.firstName)
     add("Last Name" , record.lastName )
 
-    for email in record.emailAddresses:
-        add("Email Address", email)
+    try:
+        for email in record.emailAddresses:
+            add("Email Address", email)
+    except AttributeError:
+        pass
 
-    for cua in record.calendarUserAddresses:
-        add("Calendar User Address", cua)
+    try:
+        for cua in record.calendarUserAddresses:
+            add("Calendar User Address", cua)
+    except AttributeError:
+        pass
 
     add("Server ID"           , record.serverID)
     add("Enabled"             , record.enabled)
-    add("Enabled for Calendar", record.enabledForCalendaring)
-    add("Enabled for Contacts", record.enabledForAddressBooks)
+    add("Enabled for Calendar", record.hasCalendars)
+    add("Enabled for Contacts", record.hasContacts)
 
     return succeed(table.toString())
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/terminal.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/terminal.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/terminal.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -51,7 +51,6 @@
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 from calendarserver.tools.cmdline import utilityMain, WorkerService
-from calendarserver.tools.util import getDirectory
 from calendarserver.tools.shell.cmd import Commands, UsageError as CommandUsageError
 
 log = Logger()
@@ -116,9 +115,9 @@
     @type config: L{twistedcaldav.config.Config}
     """
 
-    def __init__(self, store, directory, options, reactor, config):
+    def __init__(self, store, options, reactor, config):
         super(ShellService, self).__init__(store)
-        self.directory = directory
+        self.directory = store.directoryService()
         self.options = options
         self.reactor = reactor
         self.config = config
@@ -434,8 +433,7 @@
 
     def makeService(store):
         from twistedcaldav.config import config
-        directory = getDirectory()
-        return ShellService(store, directory, options, reactor, config)
+        return ShellService(store, options, reactor, config)
 
     print("Initializing shell...")
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/test/test_vfs.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/test/test_vfs.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/test/test_vfs.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,32 +18,53 @@
 from twisted.trial.unittest import TestCase
 from twisted.internet.defer import succeed, inlineCallbacks
 
+# from twext.who.test.test_xml import xmlService
+
+# from txdav.common.datastore.test.util import buildStore
+
 from calendarserver.tools.shell.vfs import ListEntry
 from calendarserver.tools.shell.vfs import File, Folder
-from calendarserver.tools.shell.vfs import UIDsFolder
-from calendarserver.tools.shell.terminal import ShellService
-from twistedcaldav.directory.test.test_xmlfile import XMLFileBase
-from txdav.common.datastore.test.util import buildStore
+# from calendarserver.tools.shell.vfs import UIDsFolder
+# from calendarserver.tools.shell.terminal import ShellService
 
 
+
 class TestListEntry(TestCase):
     def test_toString(self):
-        self.assertEquals(ListEntry(None, File  , "thingo").toString(), "thingo")
-        self.assertEquals(ListEntry(None, File  , "thingo", Foo="foo").toString(), "thingo")
-        self.assertEquals(ListEntry(None, Folder, "thingo").toString(), "thingo/")
-        self.assertEquals(ListEntry(None, Folder, "thingo", Foo="foo").toString(), "thingo/")
+        self.assertEquals(
+            ListEntry(None, File, "thingo").toString(),
+            "thingo"
+        )
+        self.assertEquals(
+            ListEntry(None, File, "thingo", Foo="foo").toString(),
+            "thingo"
+        )
+        self.assertEquals(
+            ListEntry(None, Folder, "thingo").toString(),
+            "thingo/"
+        )
+        self.assertEquals(
+            ListEntry(None, Folder, "thingo", Foo="foo").toString(),
+            "thingo/"
+        )
 
 
     def test_fieldNamesImplicit(self):
         # This test assumes File doesn't set list.fieldNames.
         assert not hasattr(File.list, "fieldNames")
 
-        self.assertEquals(set(ListEntry(File(None, ()), File, "thingo").fieldNames), set(("Name",)))
+        self.assertEquals(
+            set(ListEntry(File(None, ()), File, "thingo").fieldNames),
+            set(("Name",))
+        )
 
 
     def test_fieldNamesExplicit(self):
         def fieldNames(fileClass):
-            return ListEntry(fileClass(None, ()), fileClass, "thingo", Flavor="Coconut", Style="Hard")
+            return ListEntry(
+                fileClass(None, ()), fileClass, "thingo",
+                Flavor="Coconut", Style="Hard"
+            )
 
         # Full list
         class MyFile1(File):
@@ -83,14 +104,24 @@
 
         # Name first, rest sorted by field name
         self.assertEquals(
-            tuple(ListEntry(File(None, ()), File, "thingo", Flavor="Coconut", Style="Hard").toFields()),
+            tuple(
+                ListEntry(
+                    File(None, ()), File, "thingo",
+                    Flavor="Coconut", Style="Hard"
+                ).toFields()
+            ),
             ("thingo", "Coconut", "Hard")
         )
 
 
     def test_toFieldsExplicit(self):
         def fields(fileClass):
-            return tuple(ListEntry(fileClass(None, ()), fileClass, "thingo", Flavor="Coconut", Style="Hard").toFields())
+            return tuple(
+                ListEntry(
+                    fileClass(None, ()), fileClass, "thingo",
+                    Flavor="Coconut", Style="Hard"
+                ).toFields()
+            )
 
         # Full list
         class MyFile1(File):
@@ -125,34 +156,23 @@
 
 
 
-class DirectoryStubber(XMLFileBase):
-    """
-    Object which creates a stub L{IDirectoryService}.
-    """
-    def __init__(self, testCase):
-        self.testCase = testCase
-
-
-    def mktemp(self):
-        return self.testCase.mktemp()
-
-
-
 class UIDsFolderTests(TestCase):
     """
     L{UIDsFolder} contains all principals and is keyed by UID.
     """
 
-    @inlineCallbacks
-    def setUp(self):
-        """
-        Create a L{UIDsFolder}.
-        """
-        directory = DirectoryStubber(self).service()
-        self.svc = ShellService(store=(yield buildStore(self, None, directoryService=directory)),
-                                directory=directory,
-                                options=None, reactor=None, config=None)
-        self.folder = UIDsFolder(self.svc, ())
+    # @inlineCallbacks
+    # def setUp(self):
+    #     """
+    #     Create a L{UIDsFolder}.
+    #     """
+    #     directory = xmlService(self.mktemp())
+    #     self.svc = ShellService(
+    #         store=(yield buildStore(self, None, directoryService=directory)),
+    #         directory=directory,
+    #         options=None, reactor=None, config=None
+    #     )
+    #     self.folder = UIDsFolder(self.svc, ())
 
 
     @inlineCallbacks
@@ -171,8 +191,20 @@
         listing = list((yield self.folder.list()))
         self.assertEquals(
             [x.fields for x in listing],
-            [{"Record Type": "users", "Short Name": "wsanchez",
-              "Full Name": "Wilfredo Sanchez", "Name": wsanchez},
-              {"Record Type": "users", "Short Name": "dreid",
-              "Full Name": "David Reid", "Name": dreid}]
+            [
+                {
+                    "Record Type": "users",
+                    "Short Name": "wsanchez",
+                    "Full Name": "Wilfredo Sanchez",
+                    "Name": wsanchez
+                },
+                {
+                    "Record Type": "users",
+                    "Short Name": "dreid",
+                    "Full Name": "David Reid",
+                    "Name": dreid
+                },
+            ]
         )
+
+    test_list.todo = "setup() needs to be reimplemented"

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/vfs.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/vfs.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/shell/vfs.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -334,7 +334,9 @@
     def list(self):
         names = set()
 
-        for record in self.service.directory.listRecords(self.recordType):
+        for record in self.service.directory.recordsWithRecordType(
+            self.recordType
+        ):
             for shortName in record.shortNames:
                 if shortName in names:
                     continue
@@ -411,7 +413,7 @@
                 if (
                     self.record is not None and
                     self.service.config.EnableCalDAV and
-                    self.record.enabledForCalendaring
+                    self.record.hasCalendars
                 ):
                     create = True
                 else:

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/augments.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/augments.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/augments.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,12 +20,6 @@
 
 <augments>
   <record>
-    <uid>Default</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-  </record>
-  <record>
     <uid>user01</uid>
     <enable>true</enable>
     <enable-calendar>true</enable-calendar>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/caldavd.plist
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/caldavd.plist	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/caldavd.plist	2014-04-04 17:20:27 UTC (rev 13158)
@@ -85,29 +85,29 @@
     <key>ServerRoot</key>
     <string>%(ServerRoot)s</string>
 
+    <!-- Data root -->
+    <key>DataRoot</key>
+    <string>%(DataRoot)s</string>
+
     <!-- Database root -->
     <key>DatabaseRoot</key>
     <string>%(DatabaseRoot)s</string>
 
-    <!-- Data root -->
-    <key>DataRoot</key>
-    <string>Data</string>
-
     <!-- Document root -->
     <key>DocumentRoot</key>
-    <string>Documents</string>
+    <string>%(DocumentRoot)s</string>
 
     <!-- Configuration root -->
     <key>ConfigRoot</key>
-    <string>config</string>
+    <string>%(ConfigRoot)s</string>
 
     <!-- Log root -->
     <key>LogRoot</key>
-    <string>Logs</string>
+    <string>%(LogRoot)s</string>
 
     <!-- Run root -->
     <key>RunRoot</key>
-    <string>Logs/state</string>
+    <string>%(RunRoot)s</string>
 
     <!-- Child aliases -->
     <key>Aliases</key>
@@ -147,7 +147,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
-      
+
       <key>params</key>
       <dict>
         <key>xmlFile</key>
@@ -167,7 +167,7 @@
       <true/>
       <key>type</key>
       <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
-      
+
       <key>params</key>
       <dict>
         <key>xmlFile</key>
@@ -180,14 +180,14 @@
         </array>
       </dict>
     </dict>
-    
+
     <!-- Open Directory Service (Mac OS X) -->
     <!--
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
-      
+
       <key>params</key>
       <dict>
         <key>node</key>
@@ -211,7 +211,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>xmlFiles</key>
@@ -220,14 +220,14 @@
         </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>
@@ -242,7 +242,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>host</key>
@@ -258,7 +258,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>dbpath</key>
@@ -272,7 +272,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>host</key>
@@ -515,6 +515,65 @@
           </dict>
         </dict>
 
+        <key>SimpleLineNotifier</key>
+        <dict>
+          <!-- Simple line notification service (for testing) -->
+          <key>Service</key>
+          <string>twistedcaldav.notify.SimpleLineNotifierService</string>
+          <key>Enabled</key>
+          <false/>
+          <key>Port</key>
+          <integer>62308</integer>
+        </dict>
+
+        <key>XMPPNotifier</key>
+        <dict>
+          <!-- XMPP notification service -->
+          <key>Service</key>
+          <string>twistedcaldav.notify.XMPPNotifierService</string>
+          <key>Enabled</key>
+          <false/>
+
+          <!-- XMPP host and port to contact -->
+          <key>Host</key>
+          <string>xmpp.host.name</string>
+          <key>Port</key>
+          <integer>5222</integer>
+
+          <!-- Jabber ID and password for the server -->
+          <key>JID</key>
+          <string>jid at xmpp.host.name/resource</string>
+          <key>Password</key>
+          <string>password_goes_here</string>
+
+          <!-- PubSub service address -->
+          <key>ServiceAddress</key>
+          <string>pubsub.xmpp.host.name</string>
+
+          <key>NodeConfiguration</key>
+          <dict>
+            <key>pubsub#deliver_payloads</key>
+            <string>1</string>
+            <key>pubsub#persist_items</key>
+            <string>1</string>
+          </dict>
+
+          <!-- Sends a presence notification to XMPP server at this interval (prevents disconnect) -->
+          <key>KeepAliveSeconds</key>
+          <integer>120</integer>
+
+          <!-- Sends a pubsub publish to a particular heartbeat node at this interval -->
+          <key>HeartbeatMinutes</key>
+          <integer>30</integer>
+
+          <!-- List of glob-like expressions defining which XMPP JIDs can converse with the server (for debugging) -->
+          <key>AllowedJIDs</key>
+          <array>
+            <!--
+            <string>*.example.com</string>
+             -->
+          </array>
+        </dict>
       </dict>
     </dict>
 
@@ -651,6 +710,7 @@
 	<key>UsePackageTimezones</key>
 	<true/>
 
+
     <!--
         Miscellaneous items
       -->
@@ -666,7 +726,7 @@
     <!-- Support for Content-Encoding compression options as specified in RFC2616 Section 3.5 -->
     <key>ResponseCompression</key>
     <false/>
-    
+
     <!-- The retry-after value (in seconds) to return with a 503 error. -->
     <key>HTTPRetryAfter</key>
     <integer>180</integer>
@@ -705,7 +765,6 @@
     <key>ResponseCacheTimeout</key>
     <integer>30</integer> <!-- in minutes -->
 
-
     <!-- For unit tests, enable SharedConnectionPool so we don't use up shared memory -->
     <key>SharedConnectionPool</key>
     <true/>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/resources-locations.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/resources-locations.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/resources-locations.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,7 +18,110 @@
 
 <!DOCTYPE accounts SYSTEM "accounts.dtd">
 
-<accounts realm="Test Realm">
+<directory realm="Test Realm">
+  <record type="location">
+    <short-name>location01</short-name>
+    <uid>location01</uid>
+    <full-name>Room 01</full-name>
+  </record>
+  <record type="location">
+    <short-name>location02</short-name>
+    <uid>location02</uid>
+    <full-name>Room 02</full-name>
+  </record>
+  <record type="location">
+    <short-name>location03</short-name>
+    <uid>location03</uid>
+    <full-name>Room 03</full-name>
+  </record>
+  <record type="location">
+    <short-name>location04</short-name>
+    <uid>location04</uid>
+    <full-name>Room 04</full-name>
+  </record>
+  <record type="location">
+    <short-name>location05</short-name>
+    <uid>location05</uid>
+    <full-name>Room 05</full-name>
+  </record>
+  <record type="location">
+    <short-name>location06</short-name>
+    <uid>location06</uid>
+    <full-name>Room 06</full-name>
+  </record>
+  <record type="location">
+    <short-name>location07</short-name>
+    <uid>location07</uid>
+    <full-name>Room 07</full-name>
+  </record>
+  <record type="location">
+    <short-name>location08</short-name>
+    <uid>location08</uid>
+    <full-name>Room 08</full-name>
+  </record>
+  <record type="location">
+    <short-name>location09</short-name>
+    <uid>location09</uid>
+    <full-name>Room 09</full-name>
+  </record>
+  <record type="location">
+    <short-name>location10</short-name>
+    <uid>location10</uid>
+    <full-name>Room 10</full-name>
+  </record>
+
+  <record type="resource">
+    <short-name>resource01</short-name>
+    <uid>resource01</uid>
+    <full-name>Resource 01</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource02</short-name>
+    <uid>resource02</uid>
+    <full-name>Resource 02</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource03</short-name>
+    <uid>resource03</uid>
+    <full-name>Resource 03</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource04</short-name>
+    <uid>resource04</uid>
+    <full-name>Resource 04</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource05</short-name>
+    <uid>resource05</uid>
+    <full-name>Resource 05</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource06</short-name>
+    <uid>resource06</uid>
+    <full-name>Resource 06</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource07</short-name>
+    <uid>resource07</uid>
+    <full-name>Resource 07</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource08</short-name>
+    <uid>resource08</uid>
+    <full-name>Resource 08</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource09</short-name>
+    <uid>resource09</uid>
+    <full-name>Resource 09</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource10</short-name>
+    <uid>resource10</uid>
+    <full-name>Resource 10</full-name>
+  </record>
+
+  <!--
   <location repeat="10">
     <uid>location%02d</uid>
     <guid>location%02d</guid>
@@ -31,4 +134,5 @@
     <password>resource%02d</password>
     <name>Resource %02d</name>
   </resource>
-</accounts>
+-->
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/users-groups.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/users-groups.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/gateway/users-groups.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,7 +18,95 @@
 
 <!DOCTYPE accounts SYSTEM "accounts.dtd">
 
-<accounts realm="Test Realm">
+<directory realm="Test Realm">
+ <record type="user">
+    <short-name>user01</short-name>
+    <uid>user01</uid>
+    <password>user01</password>
+    <full-name>User 01</full-name>
+    <email>user01 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user02</short-name>
+    <uid>user02</uid>
+    <password>user02</password>
+    <full-name>User 02</full-name>
+    <email>user02 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user03</short-name>
+    <uid>user03</uid>
+    <password>user03</password>
+    <full-name>User 03</full-name>
+    <email>user03 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user04</short-name>
+    <uid>user04</uid>
+    <password>user04</password>
+    <full-name>User 04</full-name>
+    <email>user04 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user05</short-name>
+    <uid>user05</uid>
+    <password>user05</password>
+    <full-name>User 05</full-name>
+    <email>user05 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user06</short-name>
+    <uid>user06</uid>
+    <password>user06</password>
+    <full-name>User 06</full-name>
+    <email>user06 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user07</short-name>
+    <uid>user07</uid>
+    <password>user07</password>
+    <full-name>User 07</full-name>
+    <email>user07 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user08</short-name>
+    <uid>user08</uid>
+    <password>user08</password>
+    <full-name>User 08</full-name>
+    <email>user08 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user09</short-name>
+    <uid>user09</uid>
+    <password>user09</password>
+    <full-name>User 09</full-name>
+    <email>user09 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user10</short-name>
+    <uid>user10</uid>
+    <password>user10</password>
+    <full-name>User 10</full-name>
+    <email>user10 at example.com</email>
+  </record>
+
+  <record type="group">
+    <uid>e5a6142c-4189-4e9e-90b0-9cd0268b314b</uid>
+    <short-name>testgroup1</short-name>
+    <full-name>Group 01</full-name>
+      <member-uid type="users">user01</member-uid>
+      <member-uid type="users">user02</member-uid>
+  </record>
+  <!--
   <user repeat="10">
     <uid>user%02d</uid>
     <guid>user%02d</guid>
@@ -37,13 +125,5 @@
       <member type="users">user02</member>
     </members>
   </group>
-  <group>
-    <uid>testgroup2</uid>
-    <guid>f5a6142c-4189-4e9e-90b0-9cd0268b314b</guid>
-    <password>test</password>
-    <name>Group 02</name>
-    <members>
-      <member type="users">user01</member>
-    </members>
-  </group>
-</accounts>
+  -->
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/caldavd.plist
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/caldavd.plist	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/caldavd.plist	2014-04-04 17:20:27 UTC (rev 13158)
@@ -139,7 +139,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
-      
+
       <key>params</key>
       <dict>
         <key>xmlFile</key>
@@ -159,7 +159,7 @@
       <true/>
       <key>type</key>
       <string>twistedcaldav.directory.xmlfile.XMLDirectoryService</string>
-      
+
       <key>params</key>
       <dict>
         <key>xmlFile</key>
@@ -168,17 +168,18 @@
         <array>
             <string>resources</string>
             <string>locations</string>
+            <string>addresses</string>
         </array>
       </dict>
     </dict>
-    
+
     <!-- Open Directory Service (Mac OS X) -->
     <!--
     <key>DirectoryService</key>
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.appleopendirectory.OpenDirectoryService</string>
-      
+
       <key>params</key>
       <dict>
         <key>node</key>
@@ -202,7 +203,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.augment.AugmentXMLDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>xmlFiles</key>
@@ -211,14 +212,14 @@
         </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>
@@ -233,7 +234,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.augment.AugmentPostgreSQLDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>host</key>
@@ -249,7 +250,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.calendaruserproxy.ProxySqliteDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>dbpath</key>
@@ -263,7 +264,7 @@
     <dict>
       <key>type</key>
       <string>twistedcaldav.directory.calendaruserproxy.ProxyPostgreSQLDB</string>
-      
+
       <key>params</key>
       <dict>
         <key>host</key>
@@ -692,7 +693,7 @@
     <!-- Support for Content-Encoding compression options as specified in RFC2616 Section 3.5 -->
     <key>ResponseCompression</key>
     <false/>
-    
+
     <!-- The retry-after value (in seconds) to return with a 503 error. -->
     <key>HTTPRetryAfter</key>
     <integer>180</integer>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/resources-locations.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/resources-locations.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/resources-locations.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,7 +18,110 @@
 
 <!DOCTYPE accounts SYSTEM "accounts.dtd">
 
-<accounts realm="Test Realm">
+<directory realm="Test Realm">
+  <record type="location">
+    <short-name>location01</short-name>
+    <uid>location01</uid>
+    <full-name>Room 01</full-name>
+  </record>
+  <record type="location">
+    <short-name>location02</short-name>
+    <uid>location02</uid>
+    <full-name>Room 02</full-name>
+  </record>
+  <record type="location">
+    <short-name>location03</short-name>
+    <uid>location03</uid>
+    <full-name>Room 03</full-name>
+  </record>
+  <record type="location">
+    <short-name>location04</short-name>
+    <uid>location04</uid>
+    <full-name>Room 04</full-name>
+  </record>
+  <record type="location">
+    <short-name>location05</short-name>
+    <uid>location05</uid>
+    <full-name>Room 05</full-name>
+  </record>
+  <record type="location">
+    <short-name>location06</short-name>
+    <uid>location06</uid>
+    <full-name>Room 06</full-name>
+  </record>
+  <record type="location">
+    <short-name>location07</short-name>
+    <uid>location07</uid>
+    <full-name>Room 07</full-name>
+  </record>
+  <record type="location">
+    <short-name>location08</short-name>
+    <uid>location08</uid>
+    <full-name>Room 08</full-name>
+  </record>
+  <record type="location">
+    <short-name>location09</short-name>
+    <uid>location09</uid>
+    <full-name>Room 09</full-name>
+  </record>
+  <record type="location">
+    <short-name>location10</short-name>
+    <uid>location10</uid>
+    <full-name>Room 10</full-name>
+  </record>
+
+  <record type="resource">
+    <short-name>resource01</short-name>
+    <uid>resource01</uid>
+    <full-name>Resource 01</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource02</short-name>
+    <uid>resource02</uid>
+    <full-name>Resource 02</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource03</short-name>
+    <uid>resource03</uid>
+    <full-name>Resource 03</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource04</short-name>
+    <uid>resource04</uid>
+    <full-name>Resource 04</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource05</short-name>
+    <uid>resource05</uid>
+    <full-name>Resource 05</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource06</short-name>
+    <uid>resource06</uid>
+    <full-name>Resource 06</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource07</short-name>
+    <uid>resource07</uid>
+    <full-name>Resource 07</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource08</short-name>
+    <uid>resource08</uid>
+    <full-name>Resource 08</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource09</short-name>
+    <uid>resource09</uid>
+    <full-name>Resource 09</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource10</short-name>
+    <uid>resource10</uid>
+    <full-name>Resource 10</full-name>
+  </record>
+
+  <!--
   <location repeat="10">
     <uid>location%02d</uid>
     <guid>location%02d</guid>
@@ -31,4 +134,5 @@
     <password>resource%02d</password>
     <name>Resource %02d</name>
   </resource>
-</accounts>
+-->
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/users-groups.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/users-groups.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/principals/users-groups.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,7 +18,95 @@
 
 <!DOCTYPE accounts SYSTEM "accounts.dtd">
 
-<accounts realm="Test Realm">
+<directory realm="Test Realm">
+ <record type="user">
+    <short-name>user01</short-name>
+    <uid>user01</uid>
+    <password>user01</password>
+    <full-name>User 01</full-name>
+    <email>user01 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user02</short-name>
+    <uid>user02</uid>
+    <password>user02</password>
+    <full-name>User 02</full-name>
+    <email>user02 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user03</short-name>
+    <uid>user03</uid>
+    <password>user03</password>
+    <full-name>User 03</full-name>
+    <email>user03 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user04</short-name>
+    <uid>user04</uid>
+    <password>user04</password>
+    <full-name>User 04</full-name>
+    <email>user04 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user05</short-name>
+    <uid>user05</uid>
+    <password>user05</password>
+    <full-name>User 05</full-name>
+    <email>user05 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user06</short-name>
+    <uid>user06</uid>
+    <password>user06</password>
+    <full-name>User 06</full-name>
+    <email>user06 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user07</short-name>
+    <uid>user07</uid>
+    <password>user07</password>
+    <full-name>User 07</full-name>
+    <email>user07 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user08</short-name>
+    <uid>user08</uid>
+    <password>user08</password>
+    <full-name>User 08</full-name>
+    <email>user08 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user09</short-name>
+    <uid>user09</uid>
+    <password>user09</password>
+    <full-name>User 09</full-name>
+    <email>user09 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user10</short-name>
+    <uid>user10</uid>
+    <password>user10</password>
+    <full-name>User 10</full-name>
+    <email>user10 at example.com</email>
+  </record>
+
+  <record type="group">
+    <uid>e5a6142c-4189-4e9e-90b0-9cd0268b314b</uid>
+    <short-name>testgroup1</short-name>
+    <full-name>Group 01</full-name>
+      <member-uid type="users">user01</member-uid>
+      <member-uid type="users">user02</member-uid>
+  </record>
+  <!--
   <user repeat="10">
     <uid>user%02d</uid>
     <guid>user%02d</guid>
@@ -37,4 +125,5 @@
       <member type="users">user02</member>
     </members>
   </group>
-</accounts>
+  -->
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_agent.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_agent.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_agent.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -16,93 +16,31 @@
 
 try:
     from calendarserver.tools.agent import AgentRealm
-    from calendarserver.tools.agent import CustomDigestCredentialFactory
-    from calendarserver.tools.agent import DirectoryServiceChecker
     from calendarserver.tools.agent import InactivityDetector
     from twistedcaldav.test.util import TestCase
-    from twisted.internet.defer import inlineCallbacks
     from twisted.internet.task import Clock
-    from twisted.cred.error import UnauthorizedLogin
     from twisted.web.resource import IResource
     from twisted.web.resource import ForbiddenResource
-    RUN_TESTS = True
+
 except ImportError:
-    RUN_TESTS = False
+    pass
 
-
-
-if RUN_TESTS:
+else:
     class AgentTestCase(TestCase):
 
-        def test_CustomDigestCredentialFactory(self):
-            f = CustomDigestCredentialFactory("md5", "/Local/Default")
-            challenge = f.getChallenge(FakeRequest())
-            self.assertTrue("qop" not in challenge)
-            self.assertEquals(challenge["algorithm"], "md5")
-            self.assertEquals(challenge["realm"], "/Local/Default")
-
-        @inlineCallbacks
-        def test_DirectoryServiceChecker(self):
-            c = DirectoryServiceChecker("/Local/Default")
-            fakeOpenDirectory = FakeOpenDirectory()
-            c.directoryModule = fakeOpenDirectory
-
-            fields = {
-                "username" : "foo",
-                "realm" : "/Local/Default",
-                "nonce" : 1,
-                "uri" : "/gateway",
-                "response" : "abc",
-                "algorithm" : "md5",
-            }
-            creds = FakeCredentials("foo", fields)
-
-            # Record does not exist:
-            fakeOpenDirectory.returnThisRecord(None)
-            try:
-                yield c.requestAvatarId(creds)
-            except UnauthorizedLogin:
-                pass
-            else:
-                self.fail("Didn't raise UnauthorizedLogin")
-
-            # Record exists, but invalid credentials
-            fakeOpenDirectory.returnThisRecord("fooRecord")
-            fakeOpenDirectory.returnThisAuthResponse(False)
-            try:
-                yield c.requestAvatarId(creds)
-            except UnauthorizedLogin:
-                pass
-            else:
-                self.fail("Didn't raise UnauthorizedLogin")
-
-            # Record exists, valid credentials
-            fakeOpenDirectory.returnThisRecord("fooRecord")
-            fakeOpenDirectory.returnThisAuthResponse(True)
-            avatar = (yield c.requestAvatarId(creds))
-            self.assertEquals(avatar, "foo")
-
-            # Record exists, but missing fields in credentials
-            del creds.fields["nonce"]
-            fakeOpenDirectory.returnThisRecord("fooRecord")
-            fakeOpenDirectory.returnThisAuthResponse(False)
-            try:
-                yield c.requestAvatarId(creds)
-            except UnauthorizedLogin:
-                pass
-            else:
-                self.fail("Didn't raise UnauthorizedLogin")
-
-
         def test_AgentRealm(self):
             realm = AgentRealm("root", ["abc"])
 
             # Valid avatar
-            _ignore_interface, resource, ignored = realm.requestAvatar("abc", None, IResource)
+            _ignore_interface, resource, ignored = realm.requestAvatar(
+                "abc", None, IResource
+            )
             self.assertEquals(resource, "root")
 
             # Not allowed avatar
-            _ignore_interface, resource, ignored = realm.requestAvatar("def", None, IResource)
+            _ignore_interface, resource, ignored = realm.requestAvatar(
+                "def", None, IResource
+            )
             self.assertTrue(isinstance(resource, ForbiddenResource))
 
             # Interface unhandled
@@ -120,6 +58,7 @@
             clock = Clock()
 
             self.inactivityReached = False
+
             def becameInactive():
                 self.inactivityReached = True
 
@@ -162,8 +101,9 @@
         def returnThisAuthResponse(self, response):
             self.authResponse = response
 
-        def authenticateUserDigest(self, ignored, node, username, challenge, response,
-            method):
+        def authenticateUserDigest(
+            self, ignored, node, username, challenge, response, method
+        ):
             return self.authResponse
 
         ODNSerror = "Error"

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_calverify.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_calverify.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_calverify.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -35,7 +35,6 @@
 from txdav.common.datastore.test.util import populateCalendarsFrom
 
 from StringIO import StringIO
-import os
 
 
 OK_ICS = """BEGIN:VCALENDAR
@@ -471,20 +470,7 @@
 
     number_to_process = len(requirements["home1"]["calendar_1"])
 
-    def configure(self):
-        super(CalVerifyDataTests, self).configure()
-        self.patch(config.DirectoryService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "calverify", "accounts.xml"
-            )
-        )
-        self.patch(config.ResourceService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "calverify", "resources.xml"
-            )
-        )
 
-
     @inlineCallbacks
     def populate(self):
 
@@ -944,25 +930,7 @@
     uuid3 = "AC478592-7783-44D1-B2AE-52359B4E8415"
     uuidl1 = "75EA36BE-F71B-40F9-81F9-CF59BF40CA8F"
 
-    def configure(self):
-        super(CalVerifyMismatchTestsBase, self).configure()
-        self.patch(config.DirectoryService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "calverify", "accounts.xml"
-            )
-        )
-        self.patch(config.ResourceService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "calverify", "resources.xml"
-            )
-        )
-        self.patch(config.AugmentService.params, "xmlFiles",
-            [os.path.join(
-                os.path.dirname(__file__), "calverify", "augments.xml"
-            ), ]
-        )
 
-
     @inlineCallbacks
     def populate(self):
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_gateway.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_gateway.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_gateway.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -25,16 +25,46 @@
 from twisted.internet.defer import inlineCallbacks, Deferred, returnValue
 
 from twistedcaldav.config import config
-from twistedcaldav.test.util import TestCase, CapturingProcessProtocol
-from calendarserver.tools.util import getDirectory
+from twistedcaldav.test.util import StoreTestCase, CapturingProcessProtocol
 import plistlib
+from twistedcaldav.memcacheclient import ClientFactory
+from twistedcaldav import memcacher
+from txdav.who.idirectory import AutoScheduleMode
 
 
-class RunCommandTestCase(TestCase):
+class RunCommandTestCase(StoreTestCase):
 
-    def setUp(self):
-        super(RunCommandTestCase, self).setUp()
+    def configure(self):
+        """
+        Override the standard StoreTestCase configuration
+        """
+        self.serverRoot = self.mktemp()
+        os.mkdir(self.serverRoot)
+        absoluteServerRoot = os.path.abspath(self.serverRoot)
 
+        configRoot = os.path.join(absoluteServerRoot, "Config")
+        if not os.path.exists(configRoot):
+            os.makedirs(configRoot)
+
+        dataRoot = os.path.join(absoluteServerRoot, "Data")
+        if not os.path.exists(dataRoot):
+            os.makedirs(dataRoot)
+
+        documentRoot = os.path.join(absoluteServerRoot, "Documents")
+        if not os.path.exists(documentRoot):
+            os.makedirs(documentRoot)
+
+        logRoot = os.path.join(absoluteServerRoot, "Logs")
+        if not os.path.exists(logRoot):
+            os.makedirs(logRoot)
+
+        runRoot = os.path.join(absoluteServerRoot, "Run")
+        if not os.path.exists(runRoot):
+            os.makedirs(runRoot)
+
+        config.reset()
+        self.configInit()
+
         testRoot = os.path.join(os.path.dirname(__file__), "gateway")
         templateName = os.path.join(testRoot, "caldavd.plist")
         templateFile = open(templateName)
@@ -43,41 +73,79 @@
 
         databaseRoot = os.path.abspath("_spawned_scripts_db" + str(os.getpid()))
         newConfig = template % {
-            "ServerRoot" : os.path.abspath(config.ServerRoot),
-            "DatabaseRoot" : databaseRoot,
-            "WritablePlist" : os.path.join(os.path.abspath(config.ConfigRoot), "caldavd-writable.plist"),
+            "ServerRoot": absoluteServerRoot,
+            "DataRoot": dataRoot,
+            "DatabaseRoot": databaseRoot,
+            "DocumentRoot": documentRoot,
+            "ConfigRoot": configRoot,
+            "LogRoot": logRoot,
+            "RunRoot": runRoot,
+            "WritablePlist": os.path.join(
+                os.path.abspath(configRoot), "caldavd-writable.plist"
+            ),
         }
-        configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist"))
+        configFilePath = FilePath(
+            os.path.join(configRoot, "caldavd.plist")
+        )
+
         configFilePath.setContent(newConfig)
 
         self.configFileName = configFilePath.path
         config.load(self.configFileName)
 
-        origUsersFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "gateway", "users-groups.xml"))
-        copyUsersFile = FilePath(os.path.join(config.DataRoot, "accounts.xml"))
+        config.Memcached.Pools.Default.ClientEnabled = False
+        config.Memcached.Pools.Default.ServerEnabled = False
+        ClientFactory.allowTestCache = True
+        memcacher.Memcacher.allowTestCache = True
+        memcacher.Memcacher.memoryCacheInstance = None
+        config.DirectoryAddressBook.Enabled = False
+        config.UsePackageTimezones = True
+
+        origUsersFile = FilePath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "gateway",
+                "users-groups.xml"
+            )
+        )
+        copyUsersFile = FilePath(
+            os.path.join(config.DataRoot, "accounts.xml")
+        )
         origUsersFile.copyTo(copyUsersFile)
 
-        origResourcesFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "gateway", "resources-locations.xml"))
-        copyResourcesFile = FilePath(os.path.join(config.DataRoot, "resources.xml"))
+        origResourcesFile = FilePath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "gateway",
+                "resources-locations.xml"
+            )
+        )
+        copyResourcesFile = FilePath(
+            os.path.join(config.DataRoot, "resources.xml")
+        )
         origResourcesFile.copyTo(copyResourcesFile)
 
-        origAugmentFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "gateway", "augments.xml"))
+        origAugmentFile = FilePath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "gateway",
+                "augments.xml"
+            )
+        )
         copyAugmentFile = FilePath(os.path.join(config.DataRoot, "augments.xml"))
         origAugmentFile.copyTo(copyAugmentFile)
 
-        # Make sure trial puts the reactor in the right state, by letting it
-        # run one reactor iteration.  (Ignore me, please.)
-        d = Deferred()
-        reactor.callLater(0, d.callback, True)
-        return d
+        # # Make sure trial puts the reactor in the right state, by letting it
+        # # run one reactor iteration.  (Ignore me, please.)
+        # d = Deferred()
+        # reactor.callLater(0, d.callback, True)
+        # return d
 
 
     @inlineCallbacks
-    def runCommand(self, command, error=False,
-        script="calendarserver_command_gateway"):
+    def runCommand(
+        self, command, error=False, script="calendarserver_command_gateway"
+    ):
         """
         Run the given command by feeding it as standard input to
         calendarserver_command_gateway in a subprocess.
@@ -86,7 +154,9 @@
         if isinstance(command, unicode):
             command = command.encode("utf-8")
 
-        sourceRoot = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
+        sourceRoot = os.path.dirname(
+            os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+        )
         cmd = os.path.join(sourceRoot, "bin", script)
 
         args = [cmd, "-f", self.configFileName]
@@ -110,6 +180,12 @@
 
 class GatewayTestCase(RunCommandTestCase):
 
+    def _flush(self):
+        # Flush both XML directories
+        self.directory._directory.services[0].flush()
+        self.directory._directory.services[1].flush()
+
+
     @inlineCallbacks
     def test_getLocationAndResourceList(self):
         results = yield self.runCommand(command_getLocationAndResourceList)
@@ -125,14 +201,18 @@
     @inlineCallbacks
     def test_getLocationAttributes(self):
         yield self.runCommand(command_createLocation)
+
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
+
         results = yield self.runCommand(command_getLocationAttributes)
-        self.assertEquals(results["result"]["Capacity"], "40")
-        self.assertEquals(results["result"]["Description"], "Test Description")
+        # self.assertEquals(results["result"]["Capacity"], "40")
+        # self.assertEquals(results["result"]["Description"], "Test Description")
         self.assertEquals(results["result"]["RecordName"], ["createdlocation01"])
         self.assertEquals(results["result"]["RealName"],
             "Created Location 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
-        self.assertEquals(results["result"]["Comment"], "Test Comment")
-        self.assertEquals(results["result"]["AutoSchedule"], True)
+        # self.assertEquals(results["result"]["Comment"], "Test Comment")
+        self.assertEquals(results["result"]["AutoScheduleMode"], u"acceptIfFree")
         self.assertEquals(results["result"]["AutoAcceptGroup"], "E5A6142C-4189-4E9E-90B0-9CD0268B314B")
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
         self.assertEquals(set(results["result"]["WriteProxies"]), set(['user05', 'user06']))
@@ -147,32 +227,37 @@
     @inlineCallbacks
     def test_getResourceAttributes(self):
         yield self.runCommand(command_createResource)
+
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
+
         results = yield self.runCommand(command_getResourceAttributes)
-        self.assertEquals(results["result"]["Comment"], "Test Comment")
-        self.assertEquals(results["result"]["Type"], "Computer")
+        # self.assertEquals(results["result"]["Comment"], "Test Comment")
+        # self.assertEquals(results["result"]["Type"], "Computer")
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
         self.assertEquals(set(results["result"]["WriteProxies"]), set(['user05', 'user06']))
 
 
     @inlineCallbacks
     def test_createAddress(self):
-        directory = getDirectory()
 
-        record = directory.recordWithUID("C701069D-9CA1-4925-A1A9-5CD94767B74B")
+        record = yield self.directory.recordWithUID("C701069D-9CA1-4925-A1A9-5CD94767B74B")
         self.assertEquals(record, None)
         yield self.runCommand(command_createAddress)
 
-        directory.flushCaches()
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
 
-        record = directory.recordWithUID("C701069D-9CA1-4925-A1A9-5CD94767B74B")
-        self.assertEquals(record.fullName.decode("utf-8"),
-            "Created Address 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
+        record = yield self.directory.recordWithUID("C701069D-9CA1-4925-A1A9-5CD94767B74B")
+        self.assertEquals(
+            record.displayName,
+            "Created Address 01 %s %s" % (unichr(208), u"\ud83d\udca3")
+        )
 
-        self.assertNotEquals(record, None)
 
-        self.assertEquals(record.extras["abbreviatedName"], "Addr1")
-        self.assertEquals(record.extras["streetAddress"], "1 Infinite Loop\nCupertino, 95014\nCA")
-        self.assertEquals(record.extras["geo"], "geo:37.331,-122.030")
+        self.assertEquals(record.abbreviatedName, "Addr1")
+        self.assertEquals(record.streetAddress, "1 Infinite Loop\nCupertino, 95014\nCA")
+        self.assertEquals(record.geographicLocation, "geo:37.331,-122.030")
 
         results = yield self.runCommand(command_getAddressList)
         self.assertEquals(len(results["result"]), 1)
@@ -185,7 +270,7 @@
         results = yield self.runCommand(command_getAddressAttributes)
         self.assertEquals(results["result"]["RealName"], u'Updated Address')
         self.assertEquals(results["result"]["StreetAddress"], u'Updated Street Address')
-        self.assertEquals(results["result"]["Geo"], u'Updated Geo')
+        self.assertEquals(results["result"]["GeographicLocation"], u'Updated Geo')
 
         results = yield self.runCommand(command_deleteAddress)
 
@@ -195,29 +280,23 @@
 
     @inlineCallbacks
     def test_createLocation(self):
-        directory = getDirectory()
 
-        record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
+        record = yield self.directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
         self.assertEquals(record, None)
         yield self.runCommand(command_createLocation)
 
-        directory.flushCaches()
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
 
-        # This appears to be necessary in order for record.autoSchedule to
-        # reflect the change prior to the directory record expiration
-        augmentService = directory.serviceForRecordType(directory.recordType_locations).augmentService
-        augmentService.refresh()
+        record = yield self.directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
+        self.assertEquals(record.fullNames[0],
+            u"Created Location 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
 
-        record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
-        self.assertEquals(record.fullName.decode("utf-8"),
-            "Created Location 01 %s %s" % (unichr(208), u"\ud83d\udca3"))
-
         self.assertNotEquals(record, None)
-        self.assertEquals(record.autoSchedule, True)
+        # self.assertEquals(record.autoScheduleMode, "")
 
-        self.assertEquals(record.extras["comment"], "Test Comment")
-        self.assertEquals(record.extras["floor"], "First")
-        self.assertEquals(record.extras["capacity"], "40")
+        self.assertEquals(record.floor, u"First")
+        # self.assertEquals(record.extras["capacity"], "40")
 
         results = yield self.runCommand(command_getLocationAttributes)
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03', 'user04']))
@@ -226,88 +305,104 @@
 
     @inlineCallbacks
     def test_setLocationAttributes(self):
-        directory = getDirectory()
 
         yield self.runCommand(command_createLocation)
         yield self.runCommand(command_setLocationAttributes)
-        directory.flushCaches()
 
-        # This appears to be necessary in order for record.autoSchedule to
-        # reflect the change
-        augmentService = directory.serviceForRecordType(directory.recordType_locations).augmentService
-        augmentService.refresh()
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
 
-        record = directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
+        record = yield self.directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
 
-        self.assertEquals(record.extras["comment"], "Updated Test Comment")
-        self.assertEquals(record.extras["floor"], "Second")
-        self.assertEquals(record.extras["capacity"], "41")
-        self.assertEquals(record.extras["streetAddress"], "2 Infinite Loop\nCupertino, 95014\nCA")
-        self.assertEquals(record.autoSchedule, True)
+        # self.assertEquals(record.extras["comment"], "Updated Test Comment")
+        self.assertEquals(record.floor, "Second")
+        # self.assertEquals(record.extras["capacity"], "41")
+        self.assertEquals(record.autoScheduleMode, AutoScheduleMode.acceptIfFree)
         self.assertEquals(record.autoAcceptGroup, "F5A6142C-4189-4E9E-90B0-9CD0268B314B")
 
         results = yield self.runCommand(command_getLocationAttributes)
-        self.assertEquals(results["result"]["AutoSchedule"], True)
+        self.assertEquals(results["result"]["AutoScheduleMode"], "acceptIfFree")
         self.assertEquals(results["result"]["AutoAcceptGroup"], "F5A6142C-4189-4E9E-90B0-9CD0268B314B")
         self.assertEquals(set(results["result"]["ReadProxies"]), set(['user03']))
         self.assertEquals(set(results["result"]["WriteProxies"]), set(['user05', 'user06', 'user07']))
 
 
     @inlineCallbacks
+    def test_setAddressOnLocation(self):
+        yield self.runCommand(command_createLocation)
+        yield self.runCommand(command_createAddress)
+        yield self.runCommand(command_setAddressOnLocation)
+        results = yield self.runCommand(command_getLocationAttributes)
+        self.assertEquals(results["result"]["AssociatedAddress"], "C701069D-9CA1-4925-A1A9-5CD94767B74B")
+        self._flush()
+        record = yield self.directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
+        self.assertEquals(record.associatedAddress, "C701069D-9CA1-4925-A1A9-5CD94767B74B")
+        yield self.runCommand(command_removeAddressFromLocation)
+        results = yield self.runCommand(command_getLocationAttributes)
+        self.assertEquals(results["result"]["AssociatedAddress"], "")
+        self._flush()
+        record = yield self.directory.recordWithUID("836B1B66-2E9A-4F46-8B1C-3DD6772C20B2")
+        self.assertEquals(record.associatedAddress, u"")
+
+
+    @inlineCallbacks
     def test_destroyLocation(self):
-        directory = getDirectory()
 
-        record = directory.recordWithUID("location01")
+        record = yield self.directory.recordWithUID("location01")
         self.assertNotEquals(record, None)
 
         yield self.runCommand(command_deleteLocation)
 
-        directory.flushCaches()
-        record = directory.recordWithUID("location01")
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
+
+        record = yield self.directory.recordWithUID("location01")
         self.assertEquals(record, None)
 
 
     @inlineCallbacks
     def test_createResource(self):
-        directory = getDirectory()
 
-        record = directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
+        record = yield self.directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
         self.assertEquals(record, None)
 
         yield self.runCommand(command_createResource)
 
-        directory.flushCaches()
-        record = directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
+
+        record = yield self.directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
         self.assertNotEquals(record, None)
 
 
     @inlineCallbacks
     def test_setResourceAttributes(self):
-        directory = getDirectory()
 
         yield self.runCommand(command_createResource)
-        directory.flushCaches()
-        record = directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
-        self.assertEquals(record.fullName, "Laptop 1")
+        record = yield self.directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
+        self.assertEquals(record.displayName, "Laptop 1")
 
         yield self.runCommand(command_setResourceAttributes)
 
-        directory.flushCaches()
-        record = directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
-        self.assertEquals(record.fullName, "Updated Laptop 1")
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
 
+        record = yield self.directory.recordWithUID("AF575A61-CFA6-49E1-A0F6-B5662C9D9801")
+        self.assertEquals(record.displayName, "Updated Laptop 1")
 
+
     @inlineCallbacks
     def test_destroyResource(self):
-        directory = getDirectory()
 
-        record = directory.recordWithUID("resource01")
+        record = yield self.directory.recordWithUID("resource01")
         self.assertNotEquals(record, None)
 
         yield self.runCommand(command_deleteResource)
 
-        directory.flushCaches()
-        record = directory.recordWithUID("resource01")
+        # Tell the resources services to flush its cache and re-read XML
+        self._flush()
+
+        record = yield self.directory.recordWithUID("resource01")
         self.assertEquals(record, None)
 
 
@@ -338,9 +433,10 @@
         """
         Verify readConfig returns with only the writable keys
         """
-        results = yield self.runCommand(command_readConfig,
-            script="calendarserver_config")
-
+        results = yield self.runCommand(
+            command_readConfig,
+            script="calendarserver_config"
+        )
         self.assertEquals(results["result"]["RedirectHTTPToHTTPS"], False)
         self.assertEquals(results["result"]["EnableSearchAddressBook"], False)
         self.assertEquals(results["result"]["EnableCalDAV"], True)
@@ -360,8 +456,10 @@
         """
         Verify writeConfig updates the writable plist file only
         """
-        results = yield self.runCommand(command_writeConfig,
-            script="calendarserver_config")
+        results = yield self.runCommand(
+            command_writeConfig,
+            script="calendarserver_config"
+        )
 
         self.assertEquals(results["result"]["EnableCalDAV"], False)
         self.assertEquals(results["result"]["EnableCardDAV"], False)
@@ -383,9 +481,9 @@
         <key>command</key>
         <string>addReadProxy</string>
         <key>Principal</key>
-        <string>locations:location01</string>
+        <string>location01</string>
         <key>Proxy</key>
-        <string>users:user03</string>
+        <string>user03</string>
 </dict>
 </plist>
 """
@@ -397,9 +495,9 @@
         <key>command</key>
         <string>addWriteProxy</string>
         <key>Principal</key>
-        <string>locations:location01</string>
+        <string>location01</string>
         <key>Proxy</key>
-        <string>users:user01</string>
+        <string>user01</string>
 </dict>
 </plist>
 """
@@ -422,7 +520,7 @@
         </array>
         <key>StreetAddress</key>
         <string>1 Infinite Loop\nCupertino, 95014\nCA</string>
-        <key>Geo</key>
+        <key>GeographicLocation</key>
         <string>geo:37.331,-122.030</string>
 </dict>
 </plist>
@@ -435,8 +533,8 @@
 <dict>
         <key>command</key>
         <string>createLocation</string>
-        <key>AutoSchedule</key>
-        <true/>
+        <key>AutoScheduleMode</key>
+        <string>acceptIfFree</string>
         <key>AutoAcceptGroup</key>
         <string>E5A6142C-4189-4E9E-90B0-9CD0268B314B</string>
         <key>GeneratedUID</key>
@@ -453,19 +551,21 @@
         <string>Test Description</string>
         <key>Floor</key>
         <string>First</string>
+        <!--
         <key>Capacity</key>
         <string>40</string>
+        -->
         <key>AssociatedAddress</key>
         <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
         <key>ReadProxies</key>
         <array>
-            <string>users:user03</string>
-            <string>users:user04</string>
+            <string>user03</string>
+            <string>user04</string>
         </array>
         <key>WriteProxies</key>
         <array>
-            <string>users:user05</string>
-            <string>users:user06</string>
+            <string>user05</string>
+            <string>user06</string>
         </array>
 </dict>
 </plist>
@@ -478,31 +578,33 @@
 <dict>
         <key>command</key>
         <string>createResource</string>
-        <key>AutoSchedule</key>
-        <true/>
+        <key>AutoScheduleMode</key>
+        <string>declineIfBusy</string>
         <key>GeneratedUID</key>
         <string>AF575A61-CFA6-49E1-A0F6-B5662C9D9801</string>
         <key>RealName</key>
         <string>Laptop 1</string>
+        <!--
         <key>Comment</key>
         <string>Test Comment</string>
         <key>Description</key>
         <string>Test Description</string>
         <key>Type</key>
         <string>Computer</string>
+        -->
         <key>RecordName</key>
         <array>
                 <string>laptop1</string>
         </array>
         <key>ReadProxies</key>
         <array>
-            <string>users:user03</string>
-            <string>users:user04</string>
+            <string>user03</string>
+            <string>user04</string>
         </array>
         <key>WriteProxies</key>
         <array>
-            <string>users:user05</string>
-            <string>users:user06</string>
+            <string>user05</string>
+            <string>user06</string>
         </array>
 </dict>
 </plist>
@@ -617,9 +719,9 @@
         <key>command</key>
         <string>removeReadProxy</string>
         <key>Principal</key>
-        <string>locations:location01</string>
+        <string>location01</string>
         <key>Proxy</key>
-        <string>users:user03</string>
+        <string>user03</string>
 </dict>
 </plist>
 """
@@ -631,9 +733,9 @@
         <key>command</key>
         <string>removeWriteProxy</string>
         <key>Principal</key>
-        <string>locations:location01</string>
+        <string>location01</string>
         <key>Proxy</key>
-        <string>users:user01</string>
+        <string>user01</string>
 </dict>
 </plist>
 """
@@ -662,24 +764,53 @@
         <string>Updated Test Description</string>
         <key>Floor</key>
         <string>Second</string>
+        <!--
         <key>Capacity</key>
         <string>41</string>
-        <key>StreetAddress</key>
-        <string>2 Infinite Loop\nCupertino, 95014\nCA</string>
+        -->
         <key>ReadProxies</key>
         <array>
-            <string>users:user03</string>
+            <string>user03</string>
         </array>
         <key>WriteProxies</key>
         <array>
-            <string>users:user05</string>
-            <string>users:user06</string>
-            <string>users:user07</string>
+            <string>user05</string>
+            <string>user06</string>
+            <string>user07</string>
         </array>
 </dict>
 </plist>
 """
 
+command_setAddressOnLocation = """<?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>command</key>
+        <string>setLocationAttributes</string>
+        <key>GeneratedUID</key>
+        <string>836B1B66-2E9A-4F46-8B1C-3DD6772C20B2</string>
+        <key>AssociatedAddress</key>
+        <string>C701069D-9CA1-4925-A1A9-5CD94767B74B</string>
+</dict>
+</plist>
+"""
+
+command_removeAddressFromLocation = """<?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>command</key>
+        <string>setLocationAttributes</string>
+        <key>GeneratedUID</key>
+        <string>836B1B66-2E9A-4F46-8B1C-3DD6772C20B2</string>
+        <key>AssociatedAddress</key>
+        <string></string>
+</dict>
+</plist>
+"""
+
+
 command_getLocationAttributes = """<?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">
@@ -716,7 +847,7 @@
         <string>Updated Address</string>
         <key>StreetAddress</key>
         <string>Updated Street Address</string>
-        <key>Geo</key>
+        <key>GeographicLocation</key>
         <string>Updated Geo</string>
 
 </dict>
@@ -730,8 +861,8 @@
 <dict>
         <key>command</key>
         <string>setResourceAttributes</string>
-        <key>AutoSchedule</key>
-        <false/>
+        <key>AutoScheduleMode</key>
+        <string>acceptIfFree</string>
         <key>GeneratedUID</key>
         <string>AF575A61-CFA6-49E1-A0F6-B5662C9D9801</string>
         <key>RealName</key>

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_principals.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_principals.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_principals.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -17,29 +17,27 @@
 import os
 import sys
 
+from calendarserver.tools.principals import (
+    parseCreationArgs, matchStrings,
+    recordForPrincipalID, getProxies, setProxies
+)
 from twext.python.filepath import CachingFilePath as FilePath
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, Deferred, returnValue
-
 from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryError
-from twistedcaldav.directory import calendaruserproxy
+from twistedcaldav.test.util import (
+    TestCase, StoreTestCase, CapturingProcessProtocol, ErrorOutput
+)
 
-from twistedcaldav.test.util import TestCase, CapturingProcessProtocol, \
-    ErrorOutput
 
-from calendarserver.tap.util import directoryFromConfig
-from calendarserver.tools.principals import (parseCreationArgs, matchStrings,
-    updateRecord, principalForPrincipalID, getProxies, setProxies)
 
-
 class ManagePrincipalsTestCase(TestCase):
 
     def setUp(self):
         super(ManagePrincipalsTestCase, self).setUp()
 
-        # Since this test operates on proxy db, we need to assign the service:
-        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(os.path.abspath(self.mktemp()))
+        # # Since this test operates on proxy db, we need to assign the service:
+        # calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(os.path.abspath(self.mktemp()))
 
         testRoot = os.path.join(os.path.dirname(__file__), "principals")
         templateName = os.path.join(testRoot, "caldavd.plist")
@@ -49,11 +47,11 @@
 
         databaseRoot = os.path.abspath("_spawned_scripts_db" + str(os.getpid()))
         newConfig = template % {
-            "ServerRoot" : os.path.abspath(config.ServerRoot),
-            "DataRoot" : os.path.abspath(config.DataRoot),
-            "DatabaseRoot" : databaseRoot,
-            "DocumentRoot" : os.path.abspath(config.DocumentRoot),
-            "LogRoot" : os.path.abspath(config.LogRoot),
+            "ServerRoot": os.path.abspath(config.ServerRoot),
+            "DataRoot": os.path.abspath(config.DataRoot),
+            "DatabaseRoot": databaseRoot,
+            "DocumentRoot": os.path.abspath(config.DocumentRoot),
+            "LogRoot": os.path.abspath(config.LogRoot),
         }
         configFilePath = FilePath(os.path.join(config.ConfigRoot, "caldavd.plist"))
         configFilePath.setContent(newConfig)
@@ -61,18 +59,33 @@
         self.configFileName = configFilePath.path
         config.load(self.configFileName)
 
-        origUsersFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "principals", "users-groups.xml"))
+        origUsersFile = FilePath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "principals",
+                "users-groups.xml"
+            )
+        )
         copyUsersFile = FilePath(os.path.join(config.DataRoot, "accounts.xml"))
         origUsersFile.copyTo(copyUsersFile)
 
-        origResourcesFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "principals", "resources-locations.xml"))
+        origResourcesFile = FilePath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "principals",
+                "resources-locations.xml"
+            )
+        )
         copyResourcesFile = FilePath(os.path.join(config.DataRoot, "resources.xml"))
         origResourcesFile.copyTo(copyResourcesFile)
 
-        origAugmentFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "principals", "augments.xml"))
+        origAugmentFile = FilePath(
+            os.path.join(
+                os.path.dirname(__file__),
+                "principals",
+                "augments.xml"
+            )
+        )
         copyAugmentFile = FilePath(os.path.join(config.DataRoot, "augments.xml"))
         origAugmentFile.copyTo(copyAugmentFile)
 
@@ -114,6 +127,7 @@
         self.assertTrue("users" in results)
         self.assertTrue("locations" in results)
         self.assertTrue("resources" in results)
+        self.assertTrue("addresses" in results)
 
 
     @inlineCallbacks
@@ -133,28 +147,36 @@
 
     @inlineCallbacks
     def test_addRemove(self):
-        results = yield self.runCommand("--add", "resources", "New Resource",
-            "newresource", "edaa6ae6-011b-4d89-ace3-6b688cdd91d9")
+        results = yield self.runCommand(
+            "--add", "resources",
+            "New Resource", "newresource", "newresourceuid"
+        )
         self.assertTrue("Added 'New Resource'" in results)
 
-        results = yield self.runCommand("--get-auto-schedule",
-            "resources:newresource")
-        self.assertTrue(results.startswith('Auto-schedule for "New Resource" (resources:newresource) is true'))
+        results = yield self.runCommand(
+            "--get-auto-schedule-mode",
+            "resources:newresource"
+        )
+        self.assertTrue(
+            results.startswith(
+                'Auto-schedule mode for "New Resource" newresourceuid (resource) newresource is accept if free, decline if busy'
+            )
+        )
 
-        results = yield self.runCommand("--get-auto-schedule-mode",
-            "resources:newresource")
-        self.assertTrue(results.startswith('Auto-schedule mode for "New Resource" (resources:newresource) is default'))
-
         results = yield self.runCommand("--list-principals=resources")
         self.assertTrue("newresource" in results)
 
-        results = yield self.runCommand("--add", "resources", "New Resource",
-            "newresource1", "edaa6ae6-011b-4d89-ace3-6b688cdd91d9")
-        self.assertTrue("Duplicate guid" in results)
+        results = yield self.runCommand(
+            "--add", "resources", "New Resource",
+            "newresource1", "newresourceuid"
+        )
+        self.assertTrue("UID already in use: newresourceuid" in results)
 
-        results = yield self.runCommand("--add", "resources", "New Resource",
-            "newresource", "fdaa6ae6-011b-4d89-ace3-6b688cdd91d9")
-        self.assertTrue("Duplicate shortName" in results)
+        results = yield self.runCommand(
+            "--add", "resources", "New Resource",
+            "newresource", "uniqueuid"
+        )
+        self.assertTrue("Record name already in use" in results)
 
         results = yield self.runCommand("--remove", "resources:newresource")
         self.assertTrue("Removed 'New Resource'" in results)
@@ -165,29 +187,13 @@
 
     def test_parseCreationArgs(self):
 
-        self.assertEquals(("full name", None, None),
-            parseCreationArgs(("full name",)))
-
-        self.assertEquals(("full name", "short name", None),
-            parseCreationArgs(("full name", "short name")))
-
-        guid = "02C3DE93-E655-4856-47B76B8BB1A7BDCE"
-
-        self.assertEquals(("full name", "short name", guid),
-            parseCreationArgs(("full name", "short name", guid)))
-
-        self.assertEquals(("full name", "short name", guid),
-            parseCreationArgs(("full name", guid, "short name")))
-
-        self.assertEquals(("full name", None, guid),
-            parseCreationArgs(("full name", guid)))
-
-        self.assertRaises(
-            ValueError,
-            parseCreationArgs, ("full name", "non guid", "non guid")
+        self.assertEquals(
+            ("full name", "short name", "uid"),
+            parseCreationArgs(("full name", "short name", "uid"))
         )
 
 
+
     def test_matchStrings(self):
         self.assertEquals("abc", matchStrings("a", ("abc", "def")))
         self.assertEquals("def", matchStrings("de", ("abc", "def")))
@@ -199,161 +205,126 @@
 
     @inlineCallbacks
     def test_modifyWriteProxies(self):
-        results = yield self.runCommand("--add-write-proxy=users:user01",
-            "locations:location01")
-        self.assertTrue(results.startswith('Added "Test User 01" (users:user01) as a write proxy for "Room 01" (locations:location01)'))
+        results = yield self.runCommand(
+            "--add-write-proxy=users:user01", "locations:location01"
+        )
+        self.assertTrue(
+            results.startswith('Added "User 01" user01 (user) user01 as a write proxy for "Room 01" location01 (location) location01')
+        )
 
-        results = yield self.runCommand("--list-write-proxies",
-            "locations:location01")
-        self.assertTrue("Test User 01" in results)
+        results = yield self.runCommand(
+            "--list-write-proxies", "locations:location01"
+        )
+        self.assertTrue("User 01" in results)
 
-        results = yield self.runCommand("--remove-proxy=users:user01",
-            "locations:location01")
+        results = yield self.runCommand(
+            "--remove-proxy=users:user01", "locations:location01"
+        )
 
-        results = yield self.runCommand("--list-write-proxies",
-            "locations:location01")
-        self.assertTrue('No write proxies for "Room 01" (locations:location01)' in results)
+        results = yield self.runCommand(
+            "--list-write-proxies", "locations:location01"
+        )
+        self.assertTrue(
+            'No write proxies for "Room 01" location01 (location) location01' in results
+        )
 
 
     @inlineCallbacks
     def test_modifyReadProxies(self):
-        results = yield self.runCommand("--add-read-proxy=users:user01",
-            "locations:location01")
-        self.assertTrue(results.startswith('Added "Test User 01" (users:user01) as a read proxy for "Room 01" (locations:location01)'))
+        results = yield self.runCommand(
+            "--add-read-proxy=users:user01", "locations:location01"
+        )
+        self.assertTrue(
+            results.startswith('Added "User 01" user01 (user) user01 as a read proxy for "Room 01" location01 (location) location01')
+        )
 
-        results = yield self.runCommand("--list-read-proxies",
-            "locations:location01")
-        self.assertTrue("Test User 01" in results)
+        results = yield self.runCommand(
+            "--list-read-proxies", "locations:location01"
+        )
+        self.assertTrue("User 01" in results)
 
-        results = yield self.runCommand("--remove-proxy=users:user01",
-            "locations:location01")
+        results = yield self.runCommand(
+            "--remove-proxy=users:user01", "locations:location01"
+        )
 
-        results = yield self.runCommand("--list-read-proxies",
-            "locations:location01")
-        self.assertTrue('No read proxies for "Room 01" (locations:location01)' in results)
+        results = yield self.runCommand(
+            "--list-read-proxies", "locations:location01"
+        )
+        self.assertTrue(
+            'No read proxies for "Room 01" location01 (location) location01' in results
+        )
 
 
     @inlineCallbacks
-    def test_autoSchedule(self):
-        results = yield self.runCommand("--get-auto-schedule",
-            "locations:location01")
-        self.assertTrue(results.startswith('Auto-schedule for "Room 01" (locations:location01) is false'))
-
-        results = yield self.runCommand("--set-auto-schedule=true",
-            "locations:location01")
-        self.assertTrue(results.startswith('Setting auto-schedule to true for "Room 01" (locations:location01)'))
-
-        results = yield self.runCommand("--get-auto-schedule",
-            "locations:location01")
-        self.assertTrue(results.startswith('Auto-schedule for "Room 01" (locations:location01) is true'))
-
-        results = yield self.runCommand("--set-auto-schedule=true",
-            "users:user01")
-        self.assertTrue(results.startswith('Enabling auto-schedule for (users)user01 is not allowed.'))
-
-
-    @inlineCallbacks
     def test_autoScheduleMode(self):
-        results = yield self.runCommand("--get-auto-schedule-mode",
-            "locations:location01")
-        self.assertTrue(results.startswith('Auto-schedule mode for "Room 01" (locations:location01) is default'))
+        results = yield self.runCommand(
+            "--get-auto-schedule-mode", "locations:location01"
+        )
+        self.assertTrue(
+            results.startswith('Auto-schedule mode for "Room 01" location01 (location) location01 is accept if free, decline if busy')
+        )
 
-        results = yield self.runCommand("--set-auto-schedule-mode=accept-if-free",
-            "locations:location01")
-        self.assertTrue(results.startswith('Setting auto-schedule mode to accept-if-free for "Room 01" (locations:location01)'))
+        results = yield self.runCommand(
+            "--set-auto-schedule-mode=accept-if-free", "locations:location01"
+        )
+        self.assertTrue(
+            results.startswith('Setting auto-schedule-mode to accept if free for "Room 01" location01 (location) location01')
+        )
 
-        results = yield self.runCommand("--get-auto-schedule-mode",
-            "locations:location01")
-        self.assertTrue(results.startswith('Auto-schedule mode for "Room 01" (locations:location01) is accept-if-free'))
+        results = yield self.runCommand(
+            "--get-auto-schedule-mode",
+            "locations:location01"
+        )
+        self.assertTrue(
+            results.startswith('Auto-schedule mode for "Room 01" location01 (location) location01 is accept if free')
+        )
 
-        results = yield self.runCommand("--set-auto-schedule-mode=decline-if-busy",
-            "users:user01")
-        self.assertTrue(results.startswith('Setting auto-schedule mode for (users)user01 is not allowed.'))
+        results = yield self.runCommand(
+            "--set-auto-schedule-mode=decline-if-busy", "users:user01"
+        )
+        self.assertTrue(results.startswith('Setting auto-schedule-mode for "User 01" user01 (user) user01 is not allowed.'))
 
         try:
-            results = yield self.runCommand("--set-auto-schedule-mode=bogus",
-                "users:user01")
+            results = yield self.runCommand(
+                "--set-auto-schedule-mode=bogus",
+                "users:user01"
+            )
         except ErrorOutput:
             pass
         else:
             self.fail("Expected command failure")
 
 
-    @inlineCallbacks
-    def test_updateRecord(self):
-        directory = directoryFromConfig(config)
-        guid = "EEE28807-A8C5-46C8-A558-A08281C558A7"
 
-        (yield updateRecord(True, directory, "locations",
-            guid=guid, fullName="Test Location", shortNames=["testlocation", ],)
-        )
-        try:
-            (yield updateRecord(True, directory, "locations",
-                guid=guid, fullName="Test Location", shortNames=["testlocation", ],)
-            )
-        except DirectoryError:
-            # We're expecting an error for trying to create a record with
-            # an existing GUID
-            pass
-        else:
-            raise self.failureException("Duplicate guid expected")
+class SetProxiesTestCase(StoreTestCase):
 
-        record = directory.recordWithGUID(guid)
-        self.assertTrue(record is not None)
-        self.assertEquals(record.fullName, "Test Location")
-        self.assertTrue(record.autoSchedule)
-
-        (yield updateRecord(False, directory, "locations",
-            guid=guid, fullName="Changed", shortNames=["testlocation", ],)
-        )
-        record = directory.recordWithGUID(guid)
-        self.assertTrue(record is not None)
-        self.assertEquals(record.fullName, "Changed")
-
-        directory.destroyRecord("locations", guid=guid)
-        record = directory.recordWithGUID(guid)
-        self.assertTrue(record is None)
-
-        # Create a user, change autoSchedule
-        guid = "F0DE73A8-39D4-4830-8D32-1FA03ABA3470"
-        (yield updateRecord(True, directory, "users",
-            guid=guid, fullName="Test User", shortNames=["testuser", ],
-            autoSchedule=True)
-        )
-        record = directory.recordWithGUID(guid)
-        self.assertTrue(record is not None)
-        self.assertEquals(record.fullName, "Test User")
-        self.assertTrue(record.autoSchedule)
-
-        (yield updateRecord(False, directory, "users",
-            guid=guid, fullName="Test User", shortNames=["testuser", ],
-            autoSchedule=False)
-        )
-        record = directory.recordWithGUID(guid)
-        self.assertTrue(record is not None)
-        self.assertEquals(record.fullName, "Test User")
-        self.assertFalse(record.autoSchedule)
-
-
     @inlineCallbacks
     def test_setProxies(self):
         """
         Read and Write proxies can be set en masse
         """
-        directory = directoryFromConfig(config)
+        directory = self.directory
+        record = yield recordForPrincipalID(directory, "users:user01")
 
-        principal = principalForPrincipalID("users:user01", directory=directory)
-        readProxies, writeProxies = (yield getProxies(principal, directory=directory))
-        self.assertEquals(readProxies, []) # initially empty
-        self.assertEquals(writeProxies, []) # initially empty
+        readProxies, writeProxies = yield getProxies(record)
+        self.assertEquals(readProxies, [])  # initially empty
+        self.assertEquals(writeProxies, [])  # initially empty
 
-        (yield setProxies(None, principal, ["users:user03", "users:user04"], ["users:user05"], directory=directory))
-        readProxies, writeProxies = (yield getProxies(principal, directory=directory))
-        self.assertEquals(set(readProxies), set(["user03", "user04"]))
-        self.assertEquals(set(writeProxies), set(["user05"]))
+        readProxies = [
+            (yield recordForPrincipalID(directory, "users:user03")),
+            (yield recordForPrincipalID(directory, "users:user04")),
+        ]
+        writeProxies = [
+            (yield recordForPrincipalID(directory, "users:user05")),
+        ]
+        yield setProxies(record, readProxies, writeProxies)
 
+        readProxies, writeProxies = yield getProxies(record)
+        self.assertEquals(set([r.uid for r in readProxies]), set(["user03", "user04"]))
+        self.assertEquals(set([r.uid for r in writeProxies]), set(["user05"]))
+
         # Using None for a proxy list indicates a no-op
-        (yield setProxies(None, principal, [], None, directory=directory))
-        readProxies, writeProxies = (yield getProxies(principal, directory=directory))
-        self.assertEquals(readProxies, []) # now empty
-        self.assertEquals(set(writeProxies), set(["user05"])) # unchanged
+        yield setProxies(record, [], None)
+        readProxies, writeProxies = yield getProxies(record)
+        self.assertEquals(readProxies, [])  # now empty
+        self.assertEquals(set([r.uid for r in writeProxies]), set(["user05"]))  # unchanged

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -17,7 +17,6 @@
 
 from calendarserver.tools.purge import PurgePrincipalService
 
-from twistedcaldav.config import config
 from twistedcaldav.ical import Component
 from twistedcaldav.test.util import StoreTestCase
 
@@ -30,7 +29,6 @@
 
 from txweb2.http_headers import MimeType
 
-import os
 
 
 future = DateTime.getNowUTC()
@@ -770,7 +768,7 @@
 DTSTART;TZID=US/Pacific:20100304T120000
 DTSTAMP:20100303T195203Z
 SEQUENCE:2
-X-APPLE-DROPBOX:/calendars/__uids__/6423F94A-6B76-4A3A-815B-D52CFD77935D/dropbox/F2F14D94-B944-43D9-8F6F-97F95B2764CA.dropbox
+X-APPLE-DROPBOX:/calendars/__uids__/C76DB741-5A2A-4239-8112-10CF152AFCA4/dropbox/F2F14D94-B944-43D9-8F6F-97F95B2764CA.dropbox
 END:VEVENT
 END:VCALENDAR
 """.replace("\n", "\r\n")
@@ -800,8 +798,8 @@
     """
     Tests for purging the data belonging to a given principal
     """
-    uid = "6423F94A-6B76-4A3A-815B-D52CFD77935D"
-    uid2 = "37DB0C90-4DB1-4932-BC69-3DAB66F374F5"
+    uid = "C76DB741-5A2A-4239-8112-10CF152AFCA4"
+    uid2 = "FFED7B62-2E08-496E-BD32-B2F95FFDDB6B"
 
     metadata = {
         "accessMode": "PUBLIC",
@@ -832,48 +830,35 @@
 
         # Add attachment to attachment.ics
         self._sqlCalendarStore._dropbox_ok = True
-        home = (yield txn.calendarHomeWithUID(self.uid))
-        calendar = (yield home.calendarWithName("calendar1"))
-        event = (yield calendar.calendarObjectWithName("attachment.ics"))
-        attachment = (yield event.createAttachmentWithName("attachment.txt"))
+        home = yield txn.calendarHomeWithUID(self.uid)
+        calendar = yield home.calendarWithName("calendar1")
+        event = yield calendar.calendarObjectWithName("attachment.ics")
+        attachment = yield event.createAttachmentWithName("attachment.txt")
         t = attachment.store(MimeType("text", "x-fixture"))
         t.write("attachment")
         t.write(" text")
-        (yield t.loseConnection())
+        yield t.loseConnection()
         self._sqlCalendarStore._dropbox_ok = False
 
         # Share calendars each way
-        home2 = (yield txn.calendarHomeWithUID(self.uid2))
-        calendar2 = (yield home2.calendarWithName("calendar2"))
-        self.sharedName = (yield calendar2.shareWith(home, _BIND_MODE_WRITE))
-        self.sharedName2 = (yield calendar.shareWith(home2, _BIND_MODE_WRITE))
+        home2 = yield txn.calendarHomeWithUID(self.uid2)
+        calendar2 = yield home2.calendarWithName("calendar2")
+        self.sharedName = yield calendar2.shareWith(home, _BIND_MODE_WRITE)
+        self.sharedName2 = yield calendar.shareWith(home2, _BIND_MODE_WRITE)
 
-        (yield txn.commit())
+        yield txn.commit()
 
         txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
-        calendar2 = (yield home.childWithName(self.sharedName))
+        home = yield txn.calendarHomeWithUID(self.uid)
+        calendar2 = yield home.childWithName(self.sharedName)
         self.assertNotEquals(calendar2, None)
-        home2 = (yield txn.calendarHomeWithUID(self.uid2))
-        calendar1 = (yield home2.childWithName(self.sharedName2))
+        home2 = yield txn.calendarHomeWithUID(self.uid2)
+        calendar1 = yield home2.childWithName(self.sharedName2)
         self.assertNotEquals(calendar1, None)
-        (yield txn.commit())
+        yield txn.commit()
 
 
-    def configure(self):
-        super(PurgePrincipalTests, self).configure()
-        self.patch(config.DirectoryService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "purge", "accounts.xml"
-            )
-        )
-        self.patch(config.ResourceService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "purge", "resources.xml"
-            )
-        )
 
-
     @inlineCallbacks
     def populate(self):
         yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
@@ -888,32 +873,39 @@
 
         # Now you see it
         txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
+        home = yield txn.calendarHomeWithUID(self.uid)
         self.assertNotEquals(home, None)
-        (yield txn.commit())
+        yield txn.commit()
 
-        count, ignored = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
+        count = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
             self.rootResource, (self.uid,), verbose=False, proxies=False, completely=True))
         self.assertEquals(count, 2) # 2 events
 
         # Now you don't
         txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
+        home = yield txn.calendarHomeWithUID(self.uid)
         self.assertEquals(home, None)
         # Verify calendar1 was unshared to uid2
-        home2 = (yield txn.calendarHomeWithUID(self.uid2))
+        home2 = yield txn.calendarHomeWithUID(self.uid2)
         self.assertEquals((yield home2.childWithName(self.sharedName)), None)
-        (yield txn.commit())
+        yield txn.commit()
 
-        count, ignored = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
-            self.rootResource, (self.uid,), verbose=False, proxies=False, completely=True))
+        count = yield PurgePrincipalService.purgeUIDs(
+            self.storeUnderTest(),
+            self.directory,
+            self.rootResource,
+            (self.uid,),
+            verbose=False,
+            proxies=False,
+            completely=True
+        )
         self.assertEquals(count, 0)
 
         # And you still don't (making sure it's not provisioned)
         txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID(self.uid))
+        home = yield txn.calendarHomeWithUID(self.uid)
         self.assertEquals(home, None)
-        (yield txn.commit())
+        yield txn.commit()
 
 
     @inlineCallbacks
@@ -928,11 +920,11 @@
         txn = self._sqlCalendarStore.newTransaction()
         home = (yield txn.calendarHomeWithUID(self.uid))
         self.assertNotEquals(home, None)
-        (yield txn.commit())
+        yield txn.commit()
 
-        count, ignored = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
+        count = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
             self.rootResource, (self.uid,), verbose=False, proxies=False, completely=False))
-        self.assertEquals(count, 1) # 2 events
+        self.assertEquals(count, 1) # 1 event
 
         # Now you still see it
         txn = self._sqlCalendarStore.newTransaction()
@@ -941,14 +933,14 @@
         # Verify calendar1 was unshared to uid2
         home2 = (yield txn.calendarHomeWithUID(self.uid2))
         self.assertEquals((yield home2.childWithName(self.sharedName)), None)
-        (yield txn.commit())
+        yield txn.commit()
 
-        count, ignored = (yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
-            self.rootResource, (self.uid,), verbose=False, proxies=False, completely=False))
+        count = yield PurgePrincipalService.purgeUIDs(self.storeUnderTest(), self.directory,
+            self.rootResource, (self.uid,), verbose=False, proxies=False, completely=False)
         self.assertEquals(count, 1)
 
         # And you still do
         txn = self._sqlCalendarStore.newTransaction()
         home = (yield txn.calendarHomeWithUID(self.uid))
         self.assertNotEquals(home, None)
-        (yield txn.commit())
+        yield txn.commit()

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge_old_events.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge_old_events.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_purge_old_events.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,25 +18,22 @@
 Tests for calendarserver.tools.purge
 """
 
-from calendarserver.tools.purge import PurgeOldEventsService, PurgeAttachmentsService, \
-    PurgePrincipalService
+import os
 
+from calendarserver.tools.purge import (
+    PurgeOldEventsService, PurgeAttachmentsService, PurgePrincipalService
+)
 from pycalendar.datetime import DateTime
 from pycalendar.timezone import Timezone
-
 from twext.enterprise.dal.syntax import Update, Delete
-from txweb2.http_headers import MimeType
-
 from twisted.internet.defer import inlineCallbacks, returnValue
-
 from twistedcaldav.config import config
 from twistedcaldav.test.util import StoreTestCase
 from twistedcaldav.vcard import Component as VCardComponent
-
 from txdav.common.datastore.sql_tables import schema
 from txdav.common.datastore.test.util import populateCalendarsFrom
+from txweb2.http_headers import MimeType
 
-import os
 
 
 now = DateTime.getToday().getYear()
@@ -415,16 +412,16 @@
         # Turn off delayed indexing option so we can have some useful tests
         self.patch(config, "FreeBusyIndexDelayedExpand", False)
 
-        self.patch(config.DirectoryService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "purge", "accounts.xml"
-            )
-        )
-        self.patch(config.ResourceService.params, "xmlFile",
-            os.path.join(
-                os.path.dirname(__file__), "purge", "resources.xml"
-            )
-        )
+        # self.patch(config.DirectoryService.params, "xmlFile",
+        #     os.path.join(
+        #         os.path.dirname(__file__), "purge", "accounts.xml"
+        #     )
+        # )
+        # self.patch(config.ResourceService.params, "xmlFile",
+        #     os.path.join(
+        #         os.path.dirname(__file__), "purge", "resources.xml"
+        #     )
+        # )
 
 
     @inlineCallbacks
@@ -679,9 +676,9 @@
         (yield txn.commit())
 
         # Purge home1
-        total, ignored = (yield PurgePrincipalService.purgeUIDs(self._sqlCalendarStore, self.directory,
+        total = yield PurgePrincipalService.purgeUIDs(self._sqlCalendarStore, self.directory,
             self.rootResource, ("home1",), verbose=False, proxies=False,
-            when=DateTime(now, 4, 1, 12, 0, 0, 0, Timezone(utc=True))))
+            when=DateTime(now, 4, 1, 12, 0, 0, 0, Timezone(utc=True)))
 
         # 4 items deleted: 3 events and 1 vcard
         self.assertEquals(total, 4)
@@ -716,8 +713,8 @@
         (yield txn.commit())
 
         # Purge home1 completely
-        total, ignored = (yield PurgePrincipalService.purgeUIDs(self._sqlCalendarStore, self.directory,
-            self.rootResource, ("home1",), verbose=False, proxies=False, completely=True))
+        total = yield PurgePrincipalService.purgeUIDs(self._sqlCalendarStore, self.directory,
+            self.rootResource, ("home1",), verbose=False, proxies=False, completely=True)
 
         # 9 items deleted: 8 events and 1 vcard
         self.assertEquals(total, 9)

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_resources.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_resources.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/test/test_resources.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,19 +20,18 @@
     from twisted.internet.defer import inlineCallbacks, succeed
     from twistedcaldav.directory.directory import DirectoryService
     from twistedcaldav.test.util import TestCase
-    import dsattributes
-    strGUID = dsattributes.kDS1AttrGeneratedUID
-    strName = dsattributes.kDS1AttrDistinguishedName
-    RUN_TESTS = True
+    strGUID = "dsAttrTypeStandard:GeneratedUID"
+    strName = "dsAttrTypeStandard:RealName"
+
 except ImportError:
-    RUN_TESTS = False
+    pass
 
-
-
-if RUN_TESTS:
+else:
     class StubDirectoryRecord(object):
 
-        def __init__(self, recordType, guid=None, shortNames=None, fullName=None):
+        def __init__(
+            self, recordType, guid=None, shortNames=None, fullName=None
+        ):
             self.recordType = recordType
             self.guid = guid
             self.shortNames = shortNames
@@ -51,13 +50,16 @@
         def createRecords(self, data):
             for recordType, recordData in data:
                 guid = recordData["guid"]
-                record = StubDirectoryRecord(recordType, guid=guid,
-                    shortNames=recordData['shortNames'],
-                    fullName=recordData['fullName'])
+                record = StubDirectoryRecord(
+                    recordType, guid=guid,
+                    shortNames=recordData["shortNames"],
+                    fullName=recordData["fullName"]
+                )
                 self.records[guid] = record
 
-        def updateRecord(self, recordType, guid=None, shortNames=None,
-            fullName=None):
+        def updateRecord(
+            self, recordType, guid=None, shortNames=None, fullName=None
+        ):
             pass
 
 
@@ -92,35 +94,46 @@
         def test_migrateResources(self):
 
             data = {
-                    dsattributes.kDSStdRecordTypeResources :
-                    [
-                        ['projector1', {
-                            strGUID : '6C99E240-E915-4012-82FA-99E0F638D7EF',
-                            strName : 'Projector 1'
-                        }],
-                        ['projector2', {
-                            strGUID : '7C99E240-E915-4012-82FA-99E0F638D7EF',
-                            strName : 'Projector 2'
-                        }],
-                    ],
-                    dsattributes.kDSStdRecordTypePlaces :
-                    [
-                        ['office1', {
-                            strGUID : '8C99E240-E915-4012-82FA-99E0F638D7EF',
-                            strName : 'Office 1'
-                        }],
-                    ],
-                }
+                "dsRecTypeStandard:Resources":
+                [
+                    ["projector1", {
+                        strGUID: "6C99E240-E915-4012-82FA-99E0F638D7EF",
+                        strName: "Projector 1"
+                    }],
+                    ["projector2", {
+                        strGUID: "7C99E240-E915-4012-82FA-99E0F638D7EF",
+                        strName: "Projector 2"
+                    }],
+                ],
+                "dsRecTypeStandard:Places":
+                [
+                    ["office1", {
+                        strGUID: "8C99E240-E915-4012-82FA-99E0F638D7EF",
+                        strName: "Office 1"
+                    }],
+                ],
+            }
 
             def queryMethod(sourceService, recordType, verbose=False):
                 return data[recordType]
 
             directoryService = StubDirectoryService(StubAugmentService())
-            yield migrateResources(None, directoryService, queryMethod=queryMethod)
+            yield migrateResources(
+                None, directoryService, queryMethod=queryMethod
+            )
             for guid, recordType in (
-                ('6C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_resources),
-                ('7C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_resources),
-                ('8C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_locations),
+                (
+                    "6C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_resources
+                ),
+                (
+                    "7C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_resources
+                ),
+                (
+                    "8C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_locations
+                ),
             ):
                 self.assertTrue(guid in directoryService.records)
                 record = directoryService.records[guid]
@@ -131,27 +144,44 @@
             #
             # Add more to OD and re-migrate
             #
-            data[dsattributes.kDSStdRecordTypeResources].append(
-                ['projector3', {
-                    strGUID : '9C99E240-E915-4012-82FA-99E0F638D7EF',
-                    strName : 'Projector 3'
+            data["dsRecTypeStandard:Resources"].append(
+                ["projector3", {
+                    strGUID: "9C99E240-E915-4012-82FA-99E0F638D7EF",
+                    strName: "Projector 3"
                 }]
             )
-            data[dsattributes.kDSStdRecordTypePlaces].append(
-                ['office2', {
-                    strGUID : 'AC99E240-E915-4012-82FA-99E0F638D7EF',
-                    strName : 'Office 2'
+            data["dsRecTypeStandard:Places"].append(
+                ["office2", {
+                    strGUID: "AC99E240-E915-4012-82FA-99E0F638D7EF",
+                    strName: "Office 2"
                 }]
             )
 
-            yield migrateResources(None, directoryService, queryMethod=queryMethod)
+            yield migrateResources(
+                None, directoryService, queryMethod=queryMethod
+            )
 
             for guid, recordType in (
-                ('6C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_resources),
-                ('7C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_resources),
-                ('9C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_resources),
-                ('8C99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_locations),
-                ('AC99E240-E915-4012-82FA-99E0F638D7EF', DirectoryService.recordType_locations),
+                (
+                    "6C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_resources
+                ),
+                (
+                    "7C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_resources
+                ),
+                (
+                    "9C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_resources
+                ),
+                (
+                    "8C99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_locations
+                ),
+                (
+                    "AC99E240-E915-4012-82FA-99E0F638D7EF",
+                    DirectoryService.recordType_locations
+                ),
             ):
                 self.assertTrue(guid in directoryService.records)
                 record = directoryService.records[guid]

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/tools/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,8 +20,6 @@
 
 __all__ = [
     "loadConfig",
-    "getDirectory",
-    "dummyDirectoryRecord",
     "UsageError",
     "booleanArgument",
 ]
@@ -37,26 +35,19 @@
 from twistedcaldav.stdconfig import DEFAULT_CONFIG_FILE
 
 
-from twisted.python.filepath import FilePath
-from twisted.python.reflect import namedClass
 from twext.python.log import Logger
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from txdav.xml import element as davxml
 
-from calendarserver.provision.root import RootResource
 
 from twistedcaldav import memcachepool
-from twistedcaldav.directory import calendaruserproxy
-from twistedcaldav.directory.aggregate import AggregateDirectoryService
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
 from txdav.who.groups import schedulePolledGroupCachingUpdate
-from calendarserver.push.notifier import NotifierFactory
 
-from txdav.common.datastore.file import CommonDataStore
 
 log = Logger()
 
+
 def loadConfig(configFileName):
     """
     Helper method for command-line utilities to load configuration plist
@@ -78,145 +69,145 @@
 
 
 
-def getDirectory(config=config):
+# def getDirectory(config=config):
 
-    class MyDirectoryService (AggregateDirectoryService):
-        def getPrincipalCollection(self):
-            if not hasattr(self, "_principalCollection"):
+#     class MyDirectoryService (AggregateDirectoryService):
+#         def getPrincipalCollection(self):
+#             if not hasattr(self, "_principalCollection"):
 
-                if config.Notifications.Enabled:
-                    # FIXME: NotifierFactory needs reference to the store in order
-                    # to get a txn in order to create a Work item
-                    notifierFactory = NotifierFactory(
-                        None, config.ServerHostName,
-                        config.Notifications.CoalesceSeconds,
-                    )
-                else:
-                    notifierFactory = None
+#                 if config.Notifications.Enabled:
+#                     # FIXME: NotifierFactory needs reference to the store in order
+#                     # to get a txn in order to create a Work item
+#                     notifierFactory = NotifierFactory(
+#                         None, config.ServerHostName,
+#                         config.Notifications.CoalesceSeconds,
+#                     )
+#                 else:
+#                     notifierFactory = None
 
-                # Need a data store
-                _newStore = CommonDataStore(FilePath(config.DocumentRoot),
-                    notifierFactory, self, True, False)
-                if notifierFactory is not None:
-                    notifierFactory.store = _newStore
+#                 # Need a data store
+#                 _newStore = CommonDataStore(FilePath(config.DocumentRoot),
+#                     notifierFactory, self, True, False)
+#                 if notifierFactory is not None:
+#                     notifierFactory.store = _newStore
 
-                #
-                # Instantiating a DirectoryCalendarHomeProvisioningResource 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.directory.calendar import DirectoryCalendarHomeProvisioningResource
-                DirectoryCalendarHomeProvisioningResource(self, "/calendars/", _newStore)
+#                 #
+#                 # Instantiating a DirectoryCalendarHomeProvisioningResource 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.directory.calendar import DirectoryCalendarHomeProvisioningResource
+#                 DirectoryCalendarHomeProvisioningResource(self, "/calendars/", _newStore)
 
-                from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-                self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
+#                 from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+#                 self._principalCollection = DirectoryPrincipalProvisioningResource("/principals/", self)
 
-            return self._principalCollection
+#             return self._principalCollection
 
-        def setPrincipalCollection(self, coll):
-            # See principal.py line 237:  self.directory.principalCollection = self
-            pass
+#         def setPrincipalCollection(self, coll):
+#             # See principal.py line 237:  self.directory.principalCollection = self
+#             pass
 
-        principalCollection = property(getPrincipalCollection, setPrincipalCollection)
+#         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 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 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)
+#         def principalForCalendarUserAddress(self, cua):
+#             return self.principalCollection.principalForCalendarUserAddress(cua)
 
-        def principalForUID(self, uid):
-            return self.principalCollection.principalForUID(uid)
+#         def principalForUID(self, uid):
+#             return self.principalCollection.principalForUID(uid)
 
-    # Load augment/proxy db classes now
-    if config.AugmentService.type:
-        augmentClass = namedClass(config.AugmentService.type)
-        augmentService = augmentClass(**config.AugmentService.params)
-    else:
-        augmentService = None
+#     # Load augment/proxy db classes now
+#     if config.AugmentService.type:
+#         augmentClass = namedClass(config.AugmentService.type)
+#         augmentService = augmentClass(**config.AugmentService.params)
+#     else:
+#         augmentService = None
 
-    proxydbClass = namedClass(config.ProxyDBService.type)
-    calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
+#     proxydbClass = namedClass(config.ProxyDBService.type)
+#     calendaruserproxy.ProxyDBService = proxydbClass(**config.ProxyDBService.params)
 
-    # Wait for directory service to become available
-    BaseDirectoryService = namedClass(config.DirectoryService.type)
-    config.DirectoryService.params.augmentService = augmentService
-    directory = BaseDirectoryService(config.DirectoryService.params)
-    while not directory.isAvailable():
-        sleep(5)
+#     # Wait for directory service to become available
+#     BaseDirectoryService = namedClass(config.DirectoryService.type)
+#     config.DirectoryService.params.augmentService = augmentService
+#     directory = BaseDirectoryService(config.DirectoryService.params)
+#     while not directory.isAvailable():
+#         sleep(5)
 
-    directories = [directory]
+#     directories = [directory]
 
-    if config.ResourceService.Enabled:
-        resourceClass = namedClass(config.ResourceService.type)
-        config.ResourceService.params.augmentService = augmentService
-        resourceDirectory = resourceClass(config.ResourceService.params)
-        resourceDirectory.realmName = directory.realmName
-        directories.append(resourceDirectory)
+#     if config.ResourceService.Enabled:
+#         resourceClass = namedClass(config.ResourceService.type)
+#         config.ResourceService.params.augmentService = augmentService
+#         resourceDirectory = resourceClass(config.ResourceService.params)
+#         resourceDirectory.realmName = directory.realmName
+#         directories.append(resourceDirectory)
 
-    aggregate = MyDirectoryService(directories, None)
-    aggregate.augmentService = augmentService
+#     aggregate = MyDirectoryService(directories, None)
+#     aggregate.augmentService = augmentService
 
-    #
-    # Wire up the resource hierarchy
-    #
-    principalCollection = aggregate.getPrincipalCollection()
-    root = RootResource(
-        config.DocumentRoot,
-        principalCollections=(principalCollection,),
-    )
-    root.putChild("principals", principalCollection)
+#     #
+#     # Wire up the resource hierarchy
+#     #
+#     principalCollection = aggregate.getPrincipalCollection()
+#     root = RootResource(
+#         config.DocumentRoot,
+#         principalCollections=(principalCollection,),
+#     )
+#     root.putChild("principals", principalCollection)
 
-    # Need a data store
-    _newStore = CommonDataStore(FilePath(config.DocumentRoot), None, aggregate, True, False)
+#     # Need a data store
+#     _newStore = CommonDataStore(FilePath(config.DocumentRoot), None, aggregate, True, False)
 
-    from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
-    calendarCollection = DirectoryCalendarHomeProvisioningResource(
-        aggregate, "/calendars/",
-        _newStore,
-    )
-    root.putChild("calendars", calendarCollection)
+#     from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
+#     calendarCollection = DirectoryCalendarHomeProvisioningResource(
+#         aggregate, "/calendars/",
+#         _newStore,
+#     )
+#     root.putChild("calendars", calendarCollection)
 
-    return aggregate
+#     return aggregate
 
 
 
-class DummyDirectoryService (DirectoryService):
-    realmName = ""
-    baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
-    def recordTypes(self):
-        return ()
+# class DummyDirectoryService (DirectoryService):
+#     realmName = ""
+#     baseGUID = "51856FD4-5023-4890-94FE-4356C4AAC3E4"
+#     def recordTypes(self):
+#         return ()
 
 
-    def listRecords(self):
-        return ()
+#     def listRecords(self):
+#         return ()
 
 
-    def recordWithShortName(self):
-        return None
+#     def recordWithShortName(self):
+#         return None
 
-dummyDirectoryRecord = DirectoryRecord(
-    service=DummyDirectoryService(),
-    recordType="dummy",
-    guid="8EF0892F-7CB6-4B8E-B294-7C5A5321136A",
-    shortNames=("dummy",),
-    fullName="Dummy McDummerson",
-    firstName="Dummy",
-    lastName="McDummerson",
-)
+# dummyDirectoryRecord = DirectoryRecord(
+#     service=DummyDirectoryService(),
+#     recordType="dummy",
+#     guid="8EF0892F-7CB6-4B8E-B294-7C5A5321136A",
+#     shortNames=("dummy",),
+#     fullName="Dummy McDummerson",
+#     firstName="Dummy",
+#     lastName="McDummerson",
+# )
 
 class UsageError (StandardError):
     pass
@@ -334,6 +325,7 @@
 
 
 
+ at inlineCallbacks
 def principalForPrincipalID(principalID, checkOnly=False, directory=None):
 
     # Allow a directory parameter to be passed in, but default to config.directory
@@ -351,16 +343,16 @@
             raise ValueError("Can't resolve all paths yet")
 
         if checkOnly:
-            return None
+            returnValue(None)
 
-        return directory.principalCollection.principalForUID(uid)
+        returnValue((yield directory.principalCollection.principalForUID(uid)))
 
     if principalID.startswith("("):
         try:
             i = principalID.index(")")
 
             if checkOnly:
-                return None
+                returnValue(None)
 
             recordType = principalID[1:i]
             shortName = principalID[i + 1:]
@@ -368,34 +360,87 @@
             if not recordType or not shortName or "(" in recordType:
                 raise ValueError()
 
-            return directory.principalCollection.principalForShortName(recordType, shortName)
+            returnValue((yield directory.principalCollection.principalForShortName(recordType, shortName)))
 
         except ValueError:
             pass
 
     if ":" in principalID:
         if checkOnly:
-            return None
+            returnValue(None)
 
         recordType, shortName = principalID.split(":", 1)
 
-        return directory.principalCollection.principalForShortName(recordType, shortName)
+        returnValue((yield directory.principalCollection.principalForShortName(recordType, shortName)))
 
     try:
         UUID(principalID)
 
         if checkOnly:
-            return None
+            returnValue(None)
 
-        x = directory.principalCollection.principalForUID(principalID)
-        return x
+        returnValue((yield directory.principalCollection.principalForUID(principalID)))
     except ValueError:
         pass
 
     raise ValueError("Invalid principal identifier: %s" % (principalID,))
 
 
+ at inlineCallbacks
+def recordForPrincipalID(directory, principalID, checkOnly=False):
 
+    if principalID.startswith("/"):
+        segments = principalID.strip("/").split("/")
+        if (len(segments) == 3 and
+            segments[0] == "principals" and segments[1] == "__uids__"):
+            uid = segments[2]
+        else:
+            raise ValueError("Can't resolve all paths yet")
+
+        if checkOnly:
+            returnValue(None)
+
+        returnValue((yield directory.recordWithUID(uid)))
+
+    if principalID.startswith("("):
+        try:
+            i = principalID.index(")")
+
+            if checkOnly:
+                returnValue(None)
+
+            recordType = directory.oldNameToRecordType(principalID[1:i])
+            shortName = principalID[i + 1:]
+
+            if not recordType or not shortName or "(" in recordType:
+                raise ValueError()
+
+            returnValue((yield directory.recordWithShortName(recordType, shortName)))
+
+        except ValueError:
+            pass
+
+    if ":" in principalID:
+        if checkOnly:
+            returnValue(None)
+
+        recordType, shortName = principalID.split(":", 1)
+        recordType = directory.oldNameToRecordType(recordType)
+
+        returnValue((yield directory.recordWithShortName(recordType, shortName)))
+
+    try:
+        if checkOnly:
+            returnValue(None)
+
+        returnValue((yield directory.recordWithUID(principalID)))
+    except ValueError:
+        pass
+
+    raise ValueError("Invalid principal identifier: %s" % (principalID,))
+
+
+
 def proxySubprincipal(principal, proxyType):
     return principal.getChild("calendar-proxy-" + proxyType)
 
@@ -501,12 +546,19 @@
 
 
 def prettyPrincipal(principal):
-    record = principal.record
-    return "\"%s\" (%s:%s)" % (record.fullName, record.recordType,
-        record.shortNames[0])
+    prettyRecord(principal.record)
 
 
+def prettyRecord(record):
+    return "\"{d}\" {uid} ({rt}) {sn}".format(
+        d=record.displayName,
+        rt=record.recordType.name,
+        uid=record.uid,
+        sn=(", ".join(record.shortNames))
+    )
 
+
+
 class ProxyError(Exception):
     """
     Raised when proxy assignments cannot be performed

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/webadmin/principals.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/webadmin/principals.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/webadmin/principals.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -121,16 +121,17 @@
         self._store = store
 
 
+    @inlineCallbacks
     def getChild(self, name):
         if name == "":
-            return self
+            returnValue(self)
 
-        record = self._directory.recordWithUID(name)
+        record = yield self._directory.recordWithUID(name)
 
         if record:
-            return PrincipalResource(record, self._store)
+            returnValue(PrincipalResource(record, self._store))
         else:
-            return None
+            returnValue(None)
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/calendarserver/webcal/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/calendarserver/webcal/resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/calendarserver/webcal/resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -48,15 +48,17 @@
 class WebCalendarResource (ReadOnlyResourceMixIn, DAVFile):
 
     def defaultAccessControlList(self):
-        return davxml.ACL(
-            davxml.ACE(
-                davxml.Principal(davxml.Authenticated()),
-                davxml.Grant(
-                    davxml.Privilege(davxml.Read()),
+        return succeed(
+            davxml.ACL(
+                davxml.ACE(
+                    davxml.Principal(davxml.Authenticated()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                    ),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
                 ),
-                davxml.Protected(),
-                TwistedACLInheritable(),
-            ),
+            )
         )
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/conf/auth/accounts-test.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/conf/auth/accounts-test.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/conf/auth/accounts-test.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -16,174 +16,1716 @@
 limitations under the License.
  -->
 
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-
-<accounts realm="Test Realm">
-  <user>
-    <uid>admin</uid>
-    <guid>admin</guid>
+<directory realm="Test Realm">
+<record>
+    <uid>0C8BDE62-E600-4696-83D3-8B5ECABDFD2E</uid>
+    <guid>0C8BDE62-E600-4696-83D3-8B5ECABDFD2E</guid>
+    <short-name>admin</short-name>
     <password>admin</password>
-    <name>Super User</name>
-    <first-name>Super</first-name>
-    <last-name>User</last-name>
-  </user>
-  <user>
-    <uid>apprentice</uid>
-    <guid>apprentice</guid>
+    <full-name>Super User</full-name>
+    <email>admin at example.com</email>
+</record>
+<record>
+    <uid>29B6C503-11DF-43EC-8CCA-40C7003149CE</uid>
+    <guid>29B6C503-11DF-43EC-8CCA-40C7003149CE</guid>
+    <short-name>apprentice</short-name>
     <password>apprentice</password>
-    <name>Apprentice Super User</name>
-    <first-name>Apprentice</first-name>
-    <last-name>Super User</last-name>
-  </user>
-  <user>
-    <uid>wsanchez</uid>
-    <guid>wsanchez</guid>
-    <email-address>wsanchez at example.com</email-address>
-    <password>test</password>
-    <name>Wilfredo Sanchez Vega</name>
-    <first-name>Wilfredo</first-name>
-    <last-name>Sanchez Vega</last-name>
-  </user>
-  <user>
-    <uid>cdaboo</uid>
-    <guid>cdaboo</guid>
-    <email-address>cdaboo at example.com</email-address>
-    <password>test</password>
-    <name>Cyrus Daboo</name>
-    <first-name>Cyrus</first-name>
-    <last-name>Daboo</last-name>
-  </user>
-  <user>
-    <uid>sagen</uid>
-    <guid>sagen</guid>
-    <email-address>sagen at example.com</email-address>
-    <password>test</password>
-    <name>Morgen Sagen</name>
-    <first-name>Morgen</first-name>
-    <last-name>Sagen</last-name>
-  </user>
-  <user>
-    <uid>dre</uid>
-    <guid>andre</guid>
-    <email-address>dre at example.com</email-address>
-    <password>test</password>
-    <name>Andre LaBranche</name>
-    <first-name>Andre</first-name>
-    <last-name>LaBranche</last-name>
-  </user>
-  <user>
-    <uid>glyph</uid>
-    <guid>glyph</guid>
-    <email-address>glyph at example.com</email-address>
-    <password>test</password>
-    <name>Glyph Lefkowitz</name>
-    <first-name>Glyph</first-name>
-    <last-name>Lefkowitz</last-name>
-  </user>
-  <user>
-    <uid>i18nuser</uid>
-    <guid>i18nuser</guid>
-    <email-address>i18nuser at example.com</email-address>
+    <full-name>Apprentice Super User</full-name>
+    <email>apprentice at example.com</email>
+</record>
+<record>
+    <uid>860B3EE9-6D7C-4296-9639-E6B998074A78</uid>
+    <guid>860B3EE9-6D7C-4296-9639-E6B998074A78</guid>
+    <short-name>i18nuser</short-name>
     <password>i18nuser</password>
-    <name>まだ</name>
-    <first-name>ま</first-name>
-    <last-name>だ</last-name>
-  </user>
-  <user repeat="101">
-    <uid>user%02d</uid>
-    <uid>User %02d</uid>
-    <guid>user%02d</guid>
-    <password>user%02d</password>
-    <name>User %02d</name>
-    <first-name>User</first-name>
-    <last-name>%02d</last-name>
-    <email-address>user%02d at example.com</email-address>
-  </user>
-  <user repeat="10">
-    <uid>public%02d</uid>
-    <guid>public%02d</guid>
-    <password>public%02d</password>
-    <name>Public %02d</name>
-    <first-name>Public</first-name>
-    <last-name>%02d</last-name>
-  </user>
-  <group>
-    <uid>group01</uid>
-    <guid>group01</guid>
-    <password>group01</password>
-    <name>Group 01</name>
-    <members>
-      <member type="users">user01</member>
-    </members>
-  </group>
-  <group>
-    <uid>group02</uid>
-    <guid>group02</guid>
-    <password>group02</password>
-    <name>Group 02</name>
-    <members>
-      <member type="users">user06</member>
-      <member type="users">user07</member>
-    </members>
-  </group>
-  <group>
-    <uid>group03</uid>
-    <guid>group03</guid>
-    <password>group03</password>
-    <name>Group 03</name>
-    <members>
-      <member type="users">user08</member>
-      <member type="users">user09</member>
-    </members>
-  </group>
-  <group>
-    <uid>group04</uid>
-    <guid>group04</guid>
-    <password>group04</password>
-    <name>Group 04</name>
-    <members>
-      <member type="groups">group02</member>
-      <member type="groups">group03</member>
-      <member type="users">user10</member>
-    </members>
-  </group>
-  <group> <!-- delegategroup -->
-    <uid>group05</uid>
-    <guid>group05</guid>
-    <password>group05</password>
-    <name>Group 05</name>
-    <members>
-      <member type="groups">group06</member>
-      <member type="users">user20</member>
-    </members>
-  </group>
-  <group> <!-- delegatesubgroup -->
-    <uid>group06</uid>
-    <guid>group06</guid>
-    <password>group06</password>
-    <name>Group 06</name>
-    <members>
-      <member type="users">user21</member>
-    </members>
-  </group>
-  <group> <!-- readonlydelegategroup -->
-    <uid>group07</uid>
-    <guid>group07</guid>
-    <password>group07</password>
-    <name>Group 07</name>
-    <members>
-      <member type="users">user22</member>
-      <member type="users">user23</member>
-      <member type="users">user24</member>
-    </members>
-  </group>
-  <group>
-    <uid>disabledgroup</uid>
-    <guid>disabledgroup</guid>
-    <password>disabledgroup</password>
-    <name>Disabled Group</name>
-    <members>
-      <member type="users">user01</member>
-    </members>
-  </group>
-</accounts>
+    <full-name>まだ</full-name>
+    <email>i18nuser at example.com</email>
+</record>
+<record type="user">
+    <short-name>user01</short-name>
+    <uid>10000000-0000-0000-0000-000000000001</uid>
+    <guid>10000000-0000-0000-0000-000000000001</guid>
+    <password>user01</password>
+    <full-name>User 01</full-name>
+    <email>user01 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user02</short-name>
+    <uid>10000000-0000-0000-0000-000000000002</uid>
+    <guid>10000000-0000-0000-0000-000000000002</guid>
+    <password>user02</password>
+    <full-name>User 02</full-name>
+    <email>user02 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user03</short-name>
+    <uid>10000000-0000-0000-0000-000000000003</uid>
+    <guid>10000000-0000-0000-0000-000000000003</guid>
+    <password>user03</password>
+    <full-name>User 03</full-name>
+    <email>user03 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user04</short-name>
+    <uid>10000000-0000-0000-0000-000000000004</uid>
+    <guid>10000000-0000-0000-0000-000000000004</guid>
+    <password>user04</password>
+    <full-name>User 04</full-name>
+    <email>user04 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user05</short-name>
+    <uid>10000000-0000-0000-0000-000000000005</uid>
+    <guid>10000000-0000-0000-0000-000000000005</guid>
+    <password>user05</password>
+    <full-name>User 05</full-name>
+    <email>user05 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user06</short-name>
+    <uid>10000000-0000-0000-0000-000000000006</uid>
+    <guid>10000000-0000-0000-0000-000000000006</guid>
+    <password>user06</password>
+    <full-name>User 06</full-name>
+    <email>user06 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user07</short-name>
+    <uid>10000000-0000-0000-0000-000000000007</uid>
+    <guid>10000000-0000-0000-0000-000000000007</guid>
+    <password>user07</password>
+    <full-name>User 07</full-name>
+    <email>user07 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user08</short-name>
+    <uid>10000000-0000-0000-0000-000000000008</uid>
+    <guid>10000000-0000-0000-0000-000000000008</guid>
+    <password>user08</password>
+    <full-name>User 08</full-name>
+    <email>user08 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user09</short-name>
+    <uid>10000000-0000-0000-0000-000000000009</uid>
+    <guid>10000000-0000-0000-0000-000000000009</guid>
+    <password>user09</password>
+    <full-name>User 09</full-name>
+    <email>user09 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user10</short-name>
+    <uid>10000000-0000-0000-0000-000000000010</uid>
+    <guid>10000000-0000-0000-0000-000000000010</guid>
+    <password>user10</password>
+    <full-name>User 10</full-name>
+    <email>user10 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user11</short-name>
+    <uid>10000000-0000-0000-0000-000000000011</uid>
+    <guid>10000000-0000-0000-0000-000000000011</guid>
+    <password>user11</password>
+    <full-name>User 11</full-name>
+    <email>user11 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user12</short-name>
+    <uid>10000000-0000-0000-0000-000000000012</uid>
+    <guid>10000000-0000-0000-0000-000000000012</guid>
+    <password>user12</password>
+    <full-name>User 12</full-name>
+    <email>user12 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user13</short-name>
+    <uid>10000000-0000-0000-0000-000000000013</uid>
+    <guid>10000000-0000-0000-0000-000000000013</guid>
+    <password>user13</password>
+    <full-name>User 13</full-name>
+    <email>user13 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user14</short-name>
+    <uid>10000000-0000-0000-0000-000000000014</uid>
+    <guid>10000000-0000-0000-0000-000000000014</guid>
+    <password>user14</password>
+    <full-name>User 14</full-name>
+    <email>user14 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user15</short-name>
+    <uid>10000000-0000-0000-0000-000000000015</uid>
+    <guid>10000000-0000-0000-0000-000000000015</guid>
+    <password>user15</password>
+    <full-name>User 15</full-name>
+    <email>user15 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user16</short-name>
+    <uid>10000000-0000-0000-0000-000000000016</uid>
+    <guid>10000000-0000-0000-0000-000000000016</guid>
+    <password>user16</password>
+    <full-name>User 16</full-name>
+    <email>user16 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user17</short-name>
+    <uid>10000000-0000-0000-0000-000000000017</uid>
+    <guid>10000000-0000-0000-0000-000000000017</guid>
+    <password>user17</password>
+    <full-name>User 17</full-name>
+    <email>user17 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user18</short-name>
+    <uid>10000000-0000-0000-0000-000000000018</uid>
+    <guid>10000000-0000-0000-0000-000000000018</guid>
+    <password>user18</password>
+    <full-name>User 18</full-name>
+    <email>user18 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user19</short-name>
+    <uid>10000000-0000-0000-0000-000000000019</uid>
+    <guid>10000000-0000-0000-0000-000000000019</guid>
+    <password>user19</password>
+    <full-name>User 19</full-name>
+    <email>user19 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user20</short-name>
+    <uid>10000000-0000-0000-0000-000000000020</uid>
+    <guid>10000000-0000-0000-0000-000000000020</guid>
+    <password>user20</password>
+    <full-name>User 20</full-name>
+    <email>user20 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user21</short-name>
+    <uid>10000000-0000-0000-0000-000000000021</uid>
+    <guid>10000000-0000-0000-0000-000000000021</guid>
+    <password>user21</password>
+    <full-name>User 21</full-name>
+    <email>user21 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user22</short-name>
+    <uid>10000000-0000-0000-0000-000000000022</uid>
+    <guid>10000000-0000-0000-0000-000000000022</guid>
+    <password>user22</password>
+    <full-name>User 22</full-name>
+    <email>user22 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user23</short-name>
+    <uid>10000000-0000-0000-0000-000000000023</uid>
+    <guid>10000000-0000-0000-0000-000000000023</guid>
+    <password>user23</password>
+    <full-name>User 23</full-name>
+    <email>user23 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user24</short-name>
+    <uid>10000000-0000-0000-0000-000000000024</uid>
+    <guid>10000000-0000-0000-0000-000000000024</guid>
+    <password>user24</password>
+    <full-name>User 24</full-name>
+    <email>user24 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user25</short-name>
+    <uid>10000000-0000-0000-0000-000000000025</uid>
+    <guid>10000000-0000-0000-0000-000000000025</guid>
+    <password>user25</password>
+    <full-name>User 25</full-name>
+    <email>user25 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user26</short-name>
+    <uid>10000000-0000-0000-0000-000000000026</uid>
+    <guid>10000000-0000-0000-0000-000000000026</guid>
+    <password>user26</password>
+    <full-name>User 26</full-name>
+    <email>user26 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user27</short-name>
+    <uid>10000000-0000-0000-0000-000000000027</uid>
+    <guid>10000000-0000-0000-0000-000000000027</guid>
+    <password>user27</password>
+    <full-name>User 27</full-name>
+    <email>user27 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user28</short-name>
+    <uid>10000000-0000-0000-0000-000000000028</uid>
+    <guid>10000000-0000-0000-0000-000000000028</guid>
+    <password>user28</password>
+    <full-name>User 28</full-name>
+    <email>user28 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user29</short-name>
+    <uid>10000000-0000-0000-0000-000000000029</uid>
+    <guid>10000000-0000-0000-0000-000000000029</guid>
+    <password>user29</password>
+    <full-name>User 29</full-name>
+    <email>user29 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user30</short-name>
+    <uid>10000000-0000-0000-0000-000000000030</uid>
+    <guid>10000000-0000-0000-0000-000000000030</guid>
+    <password>user30</password>
+    <full-name>User 30</full-name>
+    <email>user30 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user31</short-name>
+    <uid>10000000-0000-0000-0000-000000000031</uid>
+    <guid>10000000-0000-0000-0000-000000000031</guid>
+    <password>user31</password>
+    <full-name>User 31</full-name>
+    <email>user31 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user32</short-name>
+    <uid>10000000-0000-0000-0000-000000000032</uid>
+    <guid>10000000-0000-0000-0000-000000000032</guid>
+    <password>user32</password>
+    <full-name>User 32</full-name>
+    <email>user32 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user33</short-name>
+    <uid>10000000-0000-0000-0000-000000000033</uid>
+    <guid>10000000-0000-0000-0000-000000000033</guid>
+    <password>user33</password>
+    <full-name>User 33</full-name>
+    <email>user33 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user34</short-name>
+    <uid>10000000-0000-0000-0000-000000000034</uid>
+    <guid>10000000-0000-0000-0000-000000000034</guid>
+    <password>user34</password>
+    <full-name>User 34</full-name>
+    <email>user34 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user35</short-name>
+    <uid>10000000-0000-0000-0000-000000000035</uid>
+    <guid>10000000-0000-0000-0000-000000000035</guid>
+    <password>user35</password>
+    <full-name>User 35</full-name>
+    <email>user35 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user36</short-name>
+    <uid>10000000-0000-0000-0000-000000000036</uid>
+    <guid>10000000-0000-0000-0000-000000000036</guid>
+    <password>user36</password>
+    <full-name>User 36</full-name>
+    <email>user36 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user37</short-name>
+    <uid>10000000-0000-0000-0000-000000000037</uid>
+    <guid>10000000-0000-0000-0000-000000000037</guid>
+    <password>user37</password>
+    <full-name>User 37</full-name>
+    <email>user37 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user38</short-name>
+    <uid>10000000-0000-0000-0000-000000000038</uid>
+    <guid>10000000-0000-0000-0000-000000000038</guid>
+    <password>user38</password>
+    <full-name>User 38</full-name>
+    <email>user38 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user39</short-name>
+    <uid>10000000-0000-0000-0000-000000000039</uid>
+    <guid>10000000-0000-0000-0000-000000000039</guid>
+    <password>user39</password>
+    <full-name>User 39</full-name>
+    <email>user39 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user40</short-name>
+    <uid>10000000-0000-0000-0000-000000000040</uid>
+    <guid>10000000-0000-0000-0000-000000000040</guid>
+    <password>user40</password>
+    <full-name>User 40</full-name>
+    <email>user40 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user41</short-name>
+    <uid>10000000-0000-0000-0000-000000000041</uid>
+    <guid>10000000-0000-0000-0000-000000000041</guid>
+    <password>user41</password>
+    <full-name>User 41</full-name>
+    <email>user41 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user42</short-name>
+    <uid>10000000-0000-0000-0000-000000000042</uid>
+    <guid>10000000-0000-0000-0000-000000000042</guid>
+    <password>user42</password>
+    <full-name>User 42</full-name>
+    <email>user42 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user43</short-name>
+    <uid>10000000-0000-0000-0000-000000000043</uid>
+    <guid>10000000-0000-0000-0000-000000000043</guid>
+    <password>user43</password>
+    <full-name>User 43</full-name>
+    <email>user43 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user44</short-name>
+    <uid>10000000-0000-0000-0000-000000000044</uid>
+    <guid>10000000-0000-0000-0000-000000000044</guid>
+    <password>user44</password>
+    <full-name>User 44</full-name>
+    <email>user44 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user45</short-name>
+    <uid>10000000-0000-0000-0000-000000000045</uid>
+    <guid>10000000-0000-0000-0000-000000000045</guid>
+    <password>user45</password>
+    <full-name>User 45</full-name>
+    <email>user45 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user46</short-name>
+    <uid>10000000-0000-0000-0000-000000000046</uid>
+    <guid>10000000-0000-0000-0000-000000000046</guid>
+    <password>user46</password>
+    <full-name>User 46</full-name>
+    <email>user46 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user47</short-name>
+    <uid>10000000-0000-0000-0000-000000000047</uid>
+    <guid>10000000-0000-0000-0000-000000000047</guid>
+    <password>user47</password>
+    <full-name>User 47</full-name>
+    <email>user47 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user48</short-name>
+    <uid>10000000-0000-0000-0000-000000000048</uid>
+    <guid>10000000-0000-0000-0000-000000000048</guid>
+    <password>user48</password>
+    <full-name>User 48</full-name>
+    <email>user48 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user49</short-name>
+    <uid>10000000-0000-0000-0000-000000000049</uid>
+    <guid>10000000-0000-0000-0000-000000000049</guid>
+    <password>user49</password>
+    <full-name>User 49</full-name>
+    <email>user49 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user50</short-name>
+    <uid>10000000-0000-0000-0000-000000000050</uid>
+    <guid>10000000-0000-0000-0000-000000000050</guid>
+    <password>user50</password>
+    <full-name>User 50</full-name>
+    <email>user50 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user51</short-name>
+    <uid>10000000-0000-0000-0000-000000000051</uid>
+    <guid>10000000-0000-0000-0000-000000000051</guid>
+    <password>user51</password>
+    <full-name>User 51</full-name>
+    <email>user51 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user52</short-name>
+    <uid>10000000-0000-0000-0000-000000000052</uid>
+    <guid>10000000-0000-0000-0000-000000000052</guid>
+    <password>user52</password>
+    <full-name>User 52</full-name>
+    <email>user52 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user53</short-name>
+    <uid>10000000-0000-0000-0000-000000000053</uid>
+    <guid>10000000-0000-0000-0000-000000000053</guid>
+    <password>user53</password>
+    <full-name>User 53</full-name>
+    <email>user53 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user54</short-name>
+    <uid>10000000-0000-0000-0000-000000000054</uid>
+    <guid>10000000-0000-0000-0000-000000000054</guid>
+    <password>user54</password>
+    <full-name>User 54</full-name>
+    <email>user54 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user55</short-name>
+    <uid>10000000-0000-0000-0000-000000000055</uid>
+    <guid>10000000-0000-0000-0000-000000000055</guid>
+    <password>user55</password>
+    <full-name>User 55</full-name>
+    <email>user55 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user56</short-name>
+    <uid>10000000-0000-0000-0000-000000000056</uid>
+    <guid>10000000-0000-0000-0000-000000000056</guid>
+    <password>user56</password>
+    <full-name>User 56</full-name>
+    <email>user56 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user57</short-name>
+    <uid>10000000-0000-0000-0000-000000000057</uid>
+    <guid>10000000-0000-0000-0000-000000000057</guid>
+    <password>user57</password>
+    <full-name>User 57</full-name>
+    <email>user57 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user58</short-name>
+    <uid>10000000-0000-0000-0000-000000000058</uid>
+    <guid>10000000-0000-0000-0000-000000000058</guid>
+    <password>user58</password>
+    <full-name>User 58</full-name>
+    <email>user58 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user59</short-name>
+    <uid>10000000-0000-0000-0000-000000000059</uid>
+    <guid>10000000-0000-0000-0000-000000000059</guid>
+    <password>user59</password>
+    <full-name>User 59</full-name>
+    <email>user59 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user60</short-name>
+    <uid>10000000-0000-0000-0000-000000000060</uid>
+    <guid>10000000-0000-0000-0000-000000000060</guid>
+    <password>user60</password>
+    <full-name>User 60</full-name>
+    <email>user60 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user61</short-name>
+    <uid>10000000-0000-0000-0000-000000000061</uid>
+    <guid>10000000-0000-0000-0000-000000000061</guid>
+    <password>user61</password>
+    <full-name>User 61</full-name>
+    <email>user61 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user62</short-name>
+    <uid>10000000-0000-0000-0000-000000000062</uid>
+    <guid>10000000-0000-0000-0000-000000000062</guid>
+    <password>user62</password>
+    <full-name>User 62</full-name>
+    <email>user62 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user63</short-name>
+    <uid>10000000-0000-0000-0000-000000000063</uid>
+    <guid>10000000-0000-0000-0000-000000000063</guid>
+    <password>user63</password>
+    <full-name>User 63</full-name>
+    <email>user63 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user64</short-name>
+    <uid>10000000-0000-0000-0000-000000000064</uid>
+    <guid>10000000-0000-0000-0000-000000000064</guid>
+    <password>user64</password>
+    <full-name>User 64</full-name>
+    <email>user64 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user65</short-name>
+    <uid>10000000-0000-0000-0000-000000000065</uid>
+    <guid>10000000-0000-0000-0000-000000000065</guid>
+    <password>user65</password>
+    <full-name>User 65</full-name>
+    <email>user65 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user66</short-name>
+    <uid>10000000-0000-0000-0000-000000000066</uid>
+    <guid>10000000-0000-0000-0000-000000000066</guid>
+    <password>user66</password>
+    <full-name>User 66</full-name>
+    <email>user66 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user67</short-name>
+    <uid>10000000-0000-0000-0000-000000000067</uid>
+    <guid>10000000-0000-0000-0000-000000000067</guid>
+    <password>user67</password>
+    <full-name>User 67</full-name>
+    <email>user67 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user68</short-name>
+    <uid>10000000-0000-0000-0000-000000000068</uid>
+    <guid>10000000-0000-0000-0000-000000000068</guid>
+    <password>user68</password>
+    <full-name>User 68</full-name>
+    <email>user68 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user69</short-name>
+    <uid>10000000-0000-0000-0000-000000000069</uid>
+    <guid>10000000-0000-0000-0000-000000000069</guid>
+    <password>user69</password>
+    <full-name>User 69</full-name>
+    <email>user69 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user70</short-name>
+    <uid>10000000-0000-0000-0000-000000000070</uid>
+    <guid>10000000-0000-0000-0000-000000000070</guid>
+    <password>user70</password>
+    <full-name>User 70</full-name>
+    <email>user70 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user71</short-name>
+    <uid>10000000-0000-0000-0000-000000000071</uid>
+    <guid>10000000-0000-0000-0000-000000000071</guid>
+    <password>user71</password>
+    <full-name>User 71</full-name>
+    <email>user71 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user72</short-name>
+    <uid>10000000-0000-0000-0000-000000000072</uid>
+    <guid>10000000-0000-0000-0000-000000000072</guid>
+    <password>user72</password>
+    <full-name>User 72</full-name>
+    <email>user72 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user73</short-name>
+    <uid>10000000-0000-0000-0000-000000000073</uid>
+    <guid>10000000-0000-0000-0000-000000000073</guid>
+    <password>user73</password>
+    <full-name>User 73</full-name>
+    <email>user73 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user74</short-name>
+    <uid>10000000-0000-0000-0000-000000000074</uid>
+    <guid>10000000-0000-0000-0000-000000000074</guid>
+    <password>user74</password>
+    <full-name>User 74</full-name>
+    <email>user74 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user75</short-name>
+    <uid>10000000-0000-0000-0000-000000000075</uid>
+    <guid>10000000-0000-0000-0000-000000000075</guid>
+    <password>user75</password>
+    <full-name>User 75</full-name>
+    <email>user75 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user76</short-name>
+    <uid>10000000-0000-0000-0000-000000000076</uid>
+    <guid>10000000-0000-0000-0000-000000000076</guid>
+    <password>user76</password>
+    <full-name>User 76</full-name>
+    <email>user76 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user77</short-name>
+    <uid>10000000-0000-0000-0000-000000000077</uid>
+    <guid>10000000-0000-0000-0000-000000000077</guid>
+    <password>user77</password>
+    <full-name>User 77</full-name>
+    <email>user77 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user78</short-name>
+    <uid>10000000-0000-0000-0000-000000000078</uid>
+    <guid>10000000-0000-0000-0000-000000000078</guid>
+    <password>user78</password>
+    <full-name>User 78</full-name>
+    <email>user78 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user79</short-name>
+    <uid>10000000-0000-0000-0000-000000000079</uid>
+    <guid>10000000-0000-0000-0000-000000000079</guid>
+    <password>user79</password>
+    <full-name>User 79</full-name>
+    <email>user79 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user80</short-name>
+    <uid>10000000-0000-0000-0000-000000000080</uid>
+    <guid>10000000-0000-0000-0000-000000000080</guid>
+    <password>user80</password>
+    <full-name>User 80</full-name>
+    <email>user80 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user81</short-name>
+    <uid>10000000-0000-0000-0000-000000000081</uid>
+    <guid>10000000-0000-0000-0000-000000000081</guid>
+    <password>user81</password>
+    <full-name>User 81</full-name>
+    <email>user81 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user82</short-name>
+    <uid>10000000-0000-0000-0000-000000000082</uid>
+    <guid>10000000-0000-0000-0000-000000000082</guid>
+    <password>user82</password>
+    <full-name>User 82</full-name>
+    <email>user82 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user83</short-name>
+    <uid>10000000-0000-0000-0000-000000000083</uid>
+    <guid>10000000-0000-0000-0000-000000000083</guid>
+    <password>user83</password>
+    <full-name>User 83</full-name>
+    <email>user83 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user84</short-name>
+    <uid>10000000-0000-0000-0000-000000000084</uid>
+    <guid>10000000-0000-0000-0000-000000000084</guid>
+    <password>user84</password>
+    <full-name>User 84</full-name>
+    <email>user84 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user85</short-name>
+    <uid>10000000-0000-0000-0000-000000000085</uid>
+    <guid>10000000-0000-0000-0000-000000000085</guid>
+    <password>user85</password>
+    <full-name>User 85</full-name>
+    <email>user85 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user86</short-name>
+    <uid>10000000-0000-0000-0000-000000000086</uid>
+    <guid>10000000-0000-0000-0000-000000000086</guid>
+    <password>user86</password>
+    <full-name>User 86</full-name>
+    <email>user86 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user87</short-name>
+    <uid>10000000-0000-0000-0000-000000000087</uid>
+    <guid>10000000-0000-0000-0000-000000000087</guid>
+    <password>user87</password>
+    <full-name>User 87</full-name>
+    <email>user87 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user88</short-name>
+    <uid>10000000-0000-0000-0000-000000000088</uid>
+    <guid>10000000-0000-0000-0000-000000000088</guid>
+    <password>user88</password>
+    <full-name>User 88</full-name>
+    <email>user88 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user89</short-name>
+    <uid>10000000-0000-0000-0000-000000000089</uid>
+    <guid>10000000-0000-0000-0000-000000000089</guid>
+    <password>user89</password>
+    <full-name>User 89</full-name>
+    <email>user89 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user90</short-name>
+    <uid>10000000-0000-0000-0000-000000000090</uid>
+    <guid>10000000-0000-0000-0000-000000000090</guid>
+    <password>user90</password>
+    <full-name>User 90</full-name>
+    <email>user90 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user91</short-name>
+    <uid>10000000-0000-0000-0000-000000000091</uid>
+    <guid>10000000-0000-0000-0000-000000000091</guid>
+    <password>user91</password>
+    <full-name>User 91</full-name>
+    <email>user91 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user92</short-name>
+    <uid>10000000-0000-0000-0000-000000000092</uid>
+    <guid>10000000-0000-0000-0000-000000000092</guid>
+    <password>user92</password>
+    <full-name>User 92</full-name>
+    <email>user92 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user93</short-name>
+    <uid>10000000-0000-0000-0000-000000000093</uid>
+    <guid>10000000-0000-0000-0000-000000000093</guid>
+    <password>user93</password>
+    <full-name>User 93</full-name>
+    <email>user93 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user94</short-name>
+    <uid>10000000-0000-0000-0000-000000000094</uid>
+    <guid>10000000-0000-0000-0000-000000000094</guid>
+    <password>user94</password>
+    <full-name>User 94</full-name>
+    <email>user94 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user95</short-name>
+    <uid>10000000-0000-0000-0000-000000000095</uid>
+    <guid>10000000-0000-0000-0000-000000000095</guid>
+    <password>user95</password>
+    <full-name>User 95</full-name>
+    <email>user95 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user96</short-name>
+    <uid>10000000-0000-0000-0000-000000000096</uid>
+    <guid>10000000-0000-0000-0000-000000000096</guid>
+    <password>user96</password>
+    <full-name>User 96</full-name>
+    <email>user96 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user97</short-name>
+    <uid>10000000-0000-0000-0000-000000000097</uid>
+    <guid>10000000-0000-0000-0000-000000000097</guid>
+    <password>user97</password>
+    <full-name>User 97</full-name>
+    <email>user97 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user98</short-name>
+    <uid>10000000-0000-0000-0000-000000000098</uid>
+    <guid>10000000-0000-0000-0000-000000000098</guid>
+    <password>user98</password>
+    <full-name>User 98</full-name>
+    <email>user98 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user99</short-name>
+    <uid>10000000-0000-0000-0000-000000000099</uid>
+    <guid>10000000-0000-0000-0000-000000000099</guid>
+    <password>user99</password>
+    <full-name>User 99</full-name>
+    <email>user99 at example.com</email>
+</record>
+<record type="user">
+    <short-name>user100</short-name>
+    <uid>10000000-0000-0000-0000-000000000100</uid>
+    <guid>10000000-0000-0000-0000-000000000100</guid>
+    <password>user100</password>
+    <full-name>User 100</full-name>
+    <email>user100 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public01</short-name>
+    <uid>50000000-0000-0000-0000-000000000001</uid>
+    <guid>50000000-0000-0000-0000-000000000001</guid>
+    <password>public01</password>
+    <full-name>Public 01</full-name>
+    <email>public01 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public02</short-name>
+    <uid>50000000-0000-0000-0000-000000000002</uid>
+    <guid>50000000-0000-0000-0000-000000000002</guid>
+    <password>public02</password>
+    <full-name>Public 02</full-name>
+    <email>public02 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public03</short-name>
+    <uid>50000000-0000-0000-0000-000000000003</uid>
+    <guid>50000000-0000-0000-0000-000000000003</guid>
+    <password>public03</password>
+    <full-name>Public 03</full-name>
+    <email>public03 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public04</short-name>
+    <uid>50000000-0000-0000-0000-000000000004</uid>
+    <guid>50000000-0000-0000-0000-000000000004</guid>
+    <password>public04</password>
+    <full-name>Public 04</full-name>
+    <email>public04 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public05</short-name>
+    <uid>50000000-0000-0000-0000-000000000005</uid>
+    <guid>50000000-0000-0000-0000-000000000005</guid>
+    <password>public05</password>
+    <full-name>Public 05</full-name>
+    <email>public05 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public06</short-name>
+    <uid>50000000-0000-0000-0000-000000000006</uid>
+    <guid>50000000-0000-0000-0000-000000000006</guid>
+    <password>public06</password>
+    <full-name>Public 06</full-name>
+    <email>public06 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public07</short-name>
+    <uid>50000000-0000-0000-0000-000000000007</uid>
+    <guid>50000000-0000-0000-0000-000000000007</guid>
+    <password>public07</password>
+    <full-name>Public 07</full-name>
+    <email>public07 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public08</short-name>
+    <uid>50000000-0000-0000-0000-000000000008</uid>
+    <guid>50000000-0000-0000-0000-000000000008</guid>
+    <password>public08</password>
+    <full-name>Public 08</full-name>
+    <email>public08 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public09</short-name>
+    <uid>50000000-0000-0000-0000-000000000009</uid>
+    <guid>50000000-0000-0000-0000-000000000009</guid>
+    <password>public09</password>
+    <full-name>Public 09</full-name>
+    <email>public09 at example.com</email>
+</record>
+<record type="user">
+    <short-name>public10</short-name>
+    <uid>50000000-0000-0000-0000-000000000010</uid>
+    <guid>50000000-0000-0000-0000-000000000010</guid>
+    <password>public10</password>
+    <full-name>Public 10</full-name>
+    <email>public10 at example.com</email>
+</record>
+<record type="group">
+    <short-name>group01</short-name>
+    <uid>20000000-0000-0000-0000-000000000001</uid>
+    <guid>20000000-0000-0000-0000-000000000001</guid>
+    <full-name>Group 01</full-name>
+    <email>group01 at example.com</email>
+    <member-uid>10000000-0000-0000-0000-000000000001</member-uid>
+</record>
+<record type="group">
+    <short-name>group02</short-name>
+    <uid>20000000-0000-0000-0000-000000000002</uid>
+    <guid>20000000-0000-0000-0000-000000000002</guid>
+    <full-name>Group 02</full-name>
+    <email>group02 at example.com</email>
+    <member-uid>10000000-0000-0000-0000-000000000006</member-uid>
+    <member-uid>10000000-0000-0000-0000-000000000007</member-uid>
+</record>
+<record type="group">
+    <short-name>group03</short-name>
+    <uid>20000000-0000-0000-0000-000000000003</uid>
+    <guid>20000000-0000-0000-0000-000000000003</guid>
+    <full-name>Group 03</full-name>
+    <email>group03 at example.com</email>
+    <member-uid>10000000-0000-0000-0000-000000000008</member-uid>
+    <member-uid>10000000-0000-0000-0000-000000000009</member-uid>
+</record>
+<record type="group">
+    <short-name>group04</short-name>
+    <uid>20000000-0000-0000-0000-000000000004</uid>
+    <guid>20000000-0000-0000-0000-000000000004</guid>
+    <full-name>Group 04</full-name>
+    <email>group04 at example.com</email>
+    <member-uid>20000000-0000-0000-0000-000000000002</member-uid>
+    <member-uid>20000000-0000-0000-0000-000000000003</member-uid>
+    <member-uid>10000000-0000-0000-0000-000000000010</member-uid>
+</record>
+<record type="group">
+    <short-name>group05</short-name>
+    <uid>20000000-0000-0000-0000-000000000005</uid>
+    <guid>20000000-0000-0000-0000-000000000005</guid>
+    <full-name>Group 05</full-name>
+    <email>group05 at example.com</email>
+    <member-uid>20000000-0000-0000-0000-000000000006</member-uid>
+    <member-uid>10000000-0000-0000-0000-000000000020</member-uid>
+</record>
+<record type="group">
+    <short-name>group06</short-name>
+    <uid>20000000-0000-0000-0000-000000000006</uid>
+    <guid>20000000-0000-0000-0000-000000000006</guid>
+    <full-name>Group 06</full-name>
+    <email>group06 at example.com</email>
+    <member-uid>10000000-0000-0000-0000-000000000021</member-uid>
+</record>
+<record type="group">
+    <short-name>group07</short-name>
+    <uid>20000000-0000-0000-0000-000000000007</uid>
+    <guid>20000000-0000-0000-0000-000000000007</guid>
+    <full-name>Group 07</full-name>
+    <email>group07 at example.com</email>
+    <member-uid>10000000-0000-0000-0000-000000000022</member-uid>
+    <member-uid>10000000-0000-0000-0000-000000000023</member-uid>
+    <member-uid>10000000-0000-0000-0000-000000000024</member-uid>
+</record>
+<record type="group">
+    <short-name>group08</short-name>
+    <uid>20000000-0000-0000-0000-000000000008</uid>
+    <guid>20000000-0000-0000-0000-000000000008</guid>
+    <full-name>Group 08</full-name>
+    <email>group08 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group09</short-name>
+    <uid>20000000-0000-0000-0000-000000000009</uid>
+    <guid>20000000-0000-0000-0000-000000000009</guid>
+    <full-name>Group 09</full-name>
+    <email>group09 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group10</short-name>
+    <uid>20000000-0000-0000-0000-000000000010</uid>
+    <guid>20000000-0000-0000-0000-000000000010</guid>
+    <full-name>Group 10</full-name>
+    <email>group10 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group11</short-name>
+    <uid>20000000-0000-0000-0000-000000000011</uid>
+    <guid>20000000-0000-0000-0000-000000000011</guid>
+    <full-name>Group 11</full-name>
+    <email>group11 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group12</short-name>
+    <uid>20000000-0000-0000-0000-000000000012</uid>
+    <guid>20000000-0000-0000-0000-000000000012</guid>
+    <full-name>Group 12</full-name>
+    <email>group12 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group13</short-name>
+    <uid>20000000-0000-0000-0000-000000000013</uid>
+    <guid>20000000-0000-0000-0000-000000000013</guid>
+    <full-name>Group 13</full-name>
+    <email>group13 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group14</short-name>
+    <uid>20000000-0000-0000-0000-000000000014</uid>
+    <guid>20000000-0000-0000-0000-000000000014</guid>
+    <full-name>Group 14</full-name>
+    <email>group14 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group15</short-name>
+    <uid>20000000-0000-0000-0000-000000000015</uid>
+    <guid>20000000-0000-0000-0000-000000000015</guid>
+    <full-name>Group 15</full-name>
+    <email>group15 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group16</short-name>
+    <uid>20000000-0000-0000-0000-000000000016</uid>
+    <guid>20000000-0000-0000-0000-000000000016</guid>
+    <full-name>Group 16</full-name>
+    <email>group16 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group17</short-name>
+    <uid>20000000-0000-0000-0000-000000000017</uid>
+    <guid>20000000-0000-0000-0000-000000000017</guid>
+    <full-name>Group 17</full-name>
+    <email>group17 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group18</short-name>
+    <uid>20000000-0000-0000-0000-000000000018</uid>
+    <guid>20000000-0000-0000-0000-000000000018</guid>
+    <full-name>Group 18</full-name>
+    <email>group18 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group19</short-name>
+    <uid>20000000-0000-0000-0000-000000000019</uid>
+    <guid>20000000-0000-0000-0000-000000000019</guid>
+    <full-name>Group 19</full-name>
+    <email>group19 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group20</short-name>
+    <uid>20000000-0000-0000-0000-000000000020</uid>
+    <guid>20000000-0000-0000-0000-000000000020</guid>
+    <full-name>Group 20</full-name>
+    <email>group20 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group21</short-name>
+    <uid>20000000-0000-0000-0000-000000000021</uid>
+    <guid>20000000-0000-0000-0000-000000000021</guid>
+    <full-name>Group 21</full-name>
+    <email>group21 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group22</short-name>
+    <uid>20000000-0000-0000-0000-000000000022</uid>
+    <guid>20000000-0000-0000-0000-000000000022</guid>
+    <full-name>Group 22</full-name>
+    <email>group22 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group23</short-name>
+    <uid>20000000-0000-0000-0000-000000000023</uid>
+    <guid>20000000-0000-0000-0000-000000000023</guid>
+    <full-name>Group 23</full-name>
+    <email>group23 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group24</short-name>
+    <uid>20000000-0000-0000-0000-000000000024</uid>
+    <guid>20000000-0000-0000-0000-000000000024</guid>
+    <full-name>Group 24</full-name>
+    <email>group24 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group25</short-name>
+    <uid>20000000-0000-0000-0000-000000000025</uid>
+    <guid>20000000-0000-0000-0000-000000000025</guid>
+    <full-name>Group 25</full-name>
+    <email>group25 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group26</short-name>
+    <uid>20000000-0000-0000-0000-000000000026</uid>
+    <guid>20000000-0000-0000-0000-000000000026</guid>
+    <full-name>Group 26</full-name>
+    <email>group26 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group27</short-name>
+    <uid>20000000-0000-0000-0000-000000000027</uid>
+    <guid>20000000-0000-0000-0000-000000000027</guid>
+    <full-name>Group 27</full-name>
+    <email>group27 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group28</short-name>
+    <uid>20000000-0000-0000-0000-000000000028</uid>
+    <guid>20000000-0000-0000-0000-000000000028</guid>
+    <full-name>Group 28</full-name>
+    <email>group28 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group29</short-name>
+    <uid>20000000-0000-0000-0000-000000000029</uid>
+    <guid>20000000-0000-0000-0000-000000000029</guid>
+    <full-name>Group 29</full-name>
+    <email>group29 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group30</short-name>
+    <uid>20000000-0000-0000-0000-000000000030</uid>
+    <guid>20000000-0000-0000-0000-000000000030</guid>
+    <full-name>Group 30</full-name>
+    <email>group30 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group31</short-name>
+    <uid>20000000-0000-0000-0000-000000000031</uid>
+    <guid>20000000-0000-0000-0000-000000000031</guid>
+    <full-name>Group 31</full-name>
+    <email>group31 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group32</short-name>
+    <uid>20000000-0000-0000-0000-000000000032</uid>
+    <guid>20000000-0000-0000-0000-000000000032</guid>
+    <full-name>Group 32</full-name>
+    <email>group32 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group33</short-name>
+    <uid>20000000-0000-0000-0000-000000000033</uid>
+    <guid>20000000-0000-0000-0000-000000000033</guid>
+    <full-name>Group 33</full-name>
+    <email>group33 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group34</short-name>
+    <uid>20000000-0000-0000-0000-000000000034</uid>
+    <guid>20000000-0000-0000-0000-000000000034</guid>
+    <full-name>Group 34</full-name>
+    <email>group34 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group35</short-name>
+    <uid>20000000-0000-0000-0000-000000000035</uid>
+    <guid>20000000-0000-0000-0000-000000000035</guid>
+    <full-name>Group 35</full-name>
+    <email>group35 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group36</short-name>
+    <uid>20000000-0000-0000-0000-000000000036</uid>
+    <guid>20000000-0000-0000-0000-000000000036</guid>
+    <full-name>Group 36</full-name>
+    <email>group36 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group37</short-name>
+    <uid>20000000-0000-0000-0000-000000000037</uid>
+    <guid>20000000-0000-0000-0000-000000000037</guid>
+    <full-name>Group 37</full-name>
+    <email>group37 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group38</short-name>
+    <uid>20000000-0000-0000-0000-000000000038</uid>
+    <guid>20000000-0000-0000-0000-000000000038</guid>
+    <full-name>Group 38</full-name>
+    <email>group38 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group39</short-name>
+    <uid>20000000-0000-0000-0000-000000000039</uid>
+    <guid>20000000-0000-0000-0000-000000000039</guid>
+    <full-name>Group 39</full-name>
+    <email>group39 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group40</short-name>
+    <uid>20000000-0000-0000-0000-000000000040</uid>
+    <guid>20000000-0000-0000-0000-000000000040</guid>
+    <full-name>Group 40</full-name>
+    <email>group40 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group41</short-name>
+    <uid>20000000-0000-0000-0000-000000000041</uid>
+    <guid>20000000-0000-0000-0000-000000000041</guid>
+    <full-name>Group 41</full-name>
+    <email>group41 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group42</short-name>
+    <uid>20000000-0000-0000-0000-000000000042</uid>
+    <guid>20000000-0000-0000-0000-000000000042</guid>
+    <full-name>Group 42</full-name>
+    <email>group42 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group43</short-name>
+    <uid>20000000-0000-0000-0000-000000000043</uid>
+    <guid>20000000-0000-0000-0000-000000000043</guid>
+    <full-name>Group 43</full-name>
+    <email>group43 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group44</short-name>
+    <uid>20000000-0000-0000-0000-000000000044</uid>
+    <guid>20000000-0000-0000-0000-000000000044</guid>
+    <full-name>Group 44</full-name>
+    <email>group44 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group45</short-name>
+    <uid>20000000-0000-0000-0000-000000000045</uid>
+    <guid>20000000-0000-0000-0000-000000000045</guid>
+    <full-name>Group 45</full-name>
+    <email>group45 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group46</short-name>
+    <uid>20000000-0000-0000-0000-000000000046</uid>
+    <guid>20000000-0000-0000-0000-000000000046</guid>
+    <full-name>Group 46</full-name>
+    <email>group46 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group47</short-name>
+    <uid>20000000-0000-0000-0000-000000000047</uid>
+    <guid>20000000-0000-0000-0000-000000000047</guid>
+    <full-name>Group 47</full-name>
+    <email>group47 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group48</short-name>
+    <uid>20000000-0000-0000-0000-000000000048</uid>
+    <guid>20000000-0000-0000-0000-000000000048</guid>
+    <full-name>Group 48</full-name>
+    <email>group48 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group49</short-name>
+    <uid>20000000-0000-0000-0000-000000000049</uid>
+    <guid>20000000-0000-0000-0000-000000000049</guid>
+    <full-name>Group 49</full-name>
+    <email>group49 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group50</short-name>
+    <uid>20000000-0000-0000-0000-000000000050</uid>
+    <guid>20000000-0000-0000-0000-000000000050</guid>
+    <full-name>Group 50</full-name>
+    <email>group50 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group51</short-name>
+    <uid>20000000-0000-0000-0000-000000000051</uid>
+    <guid>20000000-0000-0000-0000-000000000051</guid>
+    <full-name>Group 51</full-name>
+    <email>group51 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group52</short-name>
+    <uid>20000000-0000-0000-0000-000000000052</uid>
+    <guid>20000000-0000-0000-0000-000000000052</guid>
+    <full-name>Group 52</full-name>
+    <email>group52 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group53</short-name>
+    <uid>20000000-0000-0000-0000-000000000053</uid>
+    <guid>20000000-0000-0000-0000-000000000053</guid>
+    <full-name>Group 53</full-name>
+    <email>group53 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group54</short-name>
+    <uid>20000000-0000-0000-0000-000000000054</uid>
+    <guid>20000000-0000-0000-0000-000000000054</guid>
+    <full-name>Group 54</full-name>
+    <email>group54 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group55</short-name>
+    <uid>20000000-0000-0000-0000-000000000055</uid>
+    <guid>20000000-0000-0000-0000-000000000055</guid>
+    <full-name>Group 55</full-name>
+    <email>group55 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group56</short-name>
+    <uid>20000000-0000-0000-0000-000000000056</uid>
+    <guid>20000000-0000-0000-0000-000000000056</guid>
+    <full-name>Group 56</full-name>
+    <email>group56 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group57</short-name>
+    <uid>20000000-0000-0000-0000-000000000057</uid>
+    <guid>20000000-0000-0000-0000-000000000057</guid>
+    <full-name>Group 57</full-name>
+    <email>group57 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group58</short-name>
+    <uid>20000000-0000-0000-0000-000000000058</uid>
+    <guid>20000000-0000-0000-0000-000000000058</guid>
+    <full-name>Group 58</full-name>
+    <email>group58 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group59</short-name>
+    <uid>20000000-0000-0000-0000-000000000059</uid>
+    <guid>20000000-0000-0000-0000-000000000059</guid>
+    <full-name>Group 59</full-name>
+    <email>group59 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group60</short-name>
+    <uid>20000000-0000-0000-0000-000000000060</uid>
+    <guid>20000000-0000-0000-0000-000000000060</guid>
+    <full-name>Group 60</full-name>
+    <email>group60 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group61</short-name>
+    <uid>20000000-0000-0000-0000-000000000061</uid>
+    <guid>20000000-0000-0000-0000-000000000061</guid>
+    <full-name>Group 61</full-name>
+    <email>group61 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group62</short-name>
+    <uid>20000000-0000-0000-0000-000000000062</uid>
+    <guid>20000000-0000-0000-0000-000000000062</guid>
+    <full-name>Group 62</full-name>
+    <email>group62 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group63</short-name>
+    <uid>20000000-0000-0000-0000-000000000063</uid>
+    <guid>20000000-0000-0000-0000-000000000063</guid>
+    <full-name>Group 63</full-name>
+    <email>group63 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group64</short-name>
+    <uid>20000000-0000-0000-0000-000000000064</uid>
+    <guid>20000000-0000-0000-0000-000000000064</guid>
+    <full-name>Group 64</full-name>
+    <email>group64 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group65</short-name>
+    <uid>20000000-0000-0000-0000-000000000065</uid>
+    <guid>20000000-0000-0000-0000-000000000065</guid>
+    <full-name>Group 65</full-name>
+    <email>group65 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group66</short-name>
+    <uid>20000000-0000-0000-0000-000000000066</uid>
+    <guid>20000000-0000-0000-0000-000000000066</guid>
+    <full-name>Group 66</full-name>
+    <email>group66 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group67</short-name>
+    <uid>20000000-0000-0000-0000-000000000067</uid>
+    <guid>20000000-0000-0000-0000-000000000067</guid>
+    <full-name>Group 67</full-name>
+    <email>group67 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group68</short-name>
+    <uid>20000000-0000-0000-0000-000000000068</uid>
+    <guid>20000000-0000-0000-0000-000000000068</guid>
+    <full-name>Group 68</full-name>
+    <email>group68 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group69</short-name>
+    <uid>20000000-0000-0000-0000-000000000069</uid>
+    <guid>20000000-0000-0000-0000-000000000069</guid>
+    <full-name>Group 69</full-name>
+    <email>group69 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group70</short-name>
+    <uid>20000000-0000-0000-0000-000000000070</uid>
+    <guid>20000000-0000-0000-0000-000000000070</guid>
+    <full-name>Group 70</full-name>
+    <email>group70 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group71</short-name>
+    <uid>20000000-0000-0000-0000-000000000071</uid>
+    <guid>20000000-0000-0000-0000-000000000071</guid>
+    <full-name>Group 71</full-name>
+    <email>group71 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group72</short-name>
+    <uid>20000000-0000-0000-0000-000000000072</uid>
+    <guid>20000000-0000-0000-0000-000000000072</guid>
+    <full-name>Group 72</full-name>
+    <email>group72 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group73</short-name>
+    <uid>20000000-0000-0000-0000-000000000073</uid>
+    <guid>20000000-0000-0000-0000-000000000073</guid>
+    <full-name>Group 73</full-name>
+    <email>group73 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group74</short-name>
+    <uid>20000000-0000-0000-0000-000000000074</uid>
+    <guid>20000000-0000-0000-0000-000000000074</guid>
+    <full-name>Group 74</full-name>
+    <email>group74 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group75</short-name>
+    <uid>20000000-0000-0000-0000-000000000075</uid>
+    <guid>20000000-0000-0000-0000-000000000075</guid>
+    <full-name>Group 75</full-name>
+    <email>group75 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group76</short-name>
+    <uid>20000000-0000-0000-0000-000000000076</uid>
+    <guid>20000000-0000-0000-0000-000000000076</guid>
+    <full-name>Group 76</full-name>
+    <email>group76 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group77</short-name>
+    <uid>20000000-0000-0000-0000-000000000077</uid>
+    <guid>20000000-0000-0000-0000-000000000077</guid>
+    <full-name>Group 77</full-name>
+    <email>group77 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group78</short-name>
+    <uid>20000000-0000-0000-0000-000000000078</uid>
+    <guid>20000000-0000-0000-0000-000000000078</guid>
+    <full-name>Group 78</full-name>
+    <email>group78 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group79</short-name>
+    <uid>20000000-0000-0000-0000-000000000079</uid>
+    <guid>20000000-0000-0000-0000-000000000079</guid>
+    <full-name>Group 79</full-name>
+    <email>group79 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group80</short-name>
+    <uid>20000000-0000-0000-0000-000000000080</uid>
+    <guid>20000000-0000-0000-0000-000000000080</guid>
+    <full-name>Group 80</full-name>
+    <email>group80 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group81</short-name>
+    <uid>20000000-0000-0000-0000-000000000081</uid>
+    <guid>20000000-0000-0000-0000-000000000081</guid>
+    <full-name>Group 81</full-name>
+    <email>group81 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group82</short-name>
+    <uid>20000000-0000-0000-0000-000000000082</uid>
+    <guid>20000000-0000-0000-0000-000000000082</guid>
+    <full-name>Group 82</full-name>
+    <email>group82 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group83</short-name>
+    <uid>20000000-0000-0000-0000-000000000083</uid>
+    <guid>20000000-0000-0000-0000-000000000083</guid>
+    <full-name>Group 83</full-name>
+    <email>group83 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group84</short-name>
+    <uid>20000000-0000-0000-0000-000000000084</uid>
+    <guid>20000000-0000-0000-0000-000000000084</guid>
+    <full-name>Group 84</full-name>
+    <email>group84 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group85</short-name>
+    <uid>20000000-0000-0000-0000-000000000085</uid>
+    <guid>20000000-0000-0000-0000-000000000085</guid>
+    <full-name>Group 85</full-name>
+    <email>group85 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group86</short-name>
+    <uid>20000000-0000-0000-0000-000000000086</uid>
+    <guid>20000000-0000-0000-0000-000000000086</guid>
+    <full-name>Group 86</full-name>
+    <email>group86 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group87</short-name>
+    <uid>20000000-0000-0000-0000-000000000087</uid>
+    <guid>20000000-0000-0000-0000-000000000087</guid>
+    <full-name>Group 87</full-name>
+    <email>group87 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group88</short-name>
+    <uid>20000000-0000-0000-0000-000000000088</uid>
+    <guid>20000000-0000-0000-0000-000000000088</guid>
+    <full-name>Group 88</full-name>
+    <email>group88 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group89</short-name>
+    <uid>20000000-0000-0000-0000-000000000089</uid>
+    <guid>20000000-0000-0000-0000-000000000089</guid>
+    <full-name>Group 89</full-name>
+    <email>group89 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group90</short-name>
+    <uid>20000000-0000-0000-0000-000000000090</uid>
+    <guid>20000000-0000-0000-0000-000000000090</guid>
+    <full-name>Group 90</full-name>
+    <email>group90 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group91</short-name>
+    <uid>20000000-0000-0000-0000-000000000091</uid>
+    <guid>20000000-0000-0000-0000-000000000091</guid>
+    <full-name>Group 91</full-name>
+    <email>group91 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group92</short-name>
+    <uid>20000000-0000-0000-0000-000000000092</uid>
+    <guid>20000000-0000-0000-0000-000000000092</guid>
+    <full-name>Group 92</full-name>
+    <email>group92 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group93</short-name>
+    <uid>20000000-0000-0000-0000-000000000093</uid>
+    <guid>20000000-0000-0000-0000-000000000093</guid>
+    <full-name>Group 93</full-name>
+    <email>group93 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group94</short-name>
+    <uid>20000000-0000-0000-0000-000000000094</uid>
+    <guid>20000000-0000-0000-0000-000000000094</guid>
+    <full-name>Group 94</full-name>
+    <email>group94 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group95</short-name>
+    <uid>20000000-0000-0000-0000-000000000095</uid>
+    <guid>20000000-0000-0000-0000-000000000095</guid>
+    <full-name>Group 95</full-name>
+    <email>group95 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group96</short-name>
+    <uid>20000000-0000-0000-0000-000000000096</uid>
+    <guid>20000000-0000-0000-0000-000000000096</guid>
+    <full-name>Group 96</full-name>
+    <email>group96 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group97</short-name>
+    <uid>20000000-0000-0000-0000-000000000097</uid>
+    <guid>20000000-0000-0000-0000-000000000097</guid>
+    <full-name>Group 97</full-name>
+    <email>group97 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group98</short-name>
+    <uid>20000000-0000-0000-0000-000000000098</uid>
+    <guid>20000000-0000-0000-0000-000000000098</guid>
+    <full-name>Group 98</full-name>
+    <email>group98 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group99</short-name>
+    <uid>20000000-0000-0000-0000-000000000099</uid>
+    <guid>20000000-0000-0000-0000-000000000099</guid>
+    <full-name>Group 99</full-name>
+    <email>group99 at example.com</email>
+    
+</record>
+<record type="group">
+    <short-name>group100</short-name>
+    <uid>20000000-0000-0000-0000-000000000100</uid>
+    <guid>20000000-0000-0000-0000-000000000100</guid>
+    <full-name>Group 100</full-name>
+    <email>group100 at example.com</email>
+    
+</record>
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/conf/auth/augments-test.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/conf/auth/augments-test.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,185 +1,80 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE augments SYSTEM "augments.dtd">
 
+<!--
+Copyright (c) 2006-2014 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.
+ -->
+
 <augments>
-  <record>
+<record>
     <uid>Default</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
-  </record>
-  <record repeat="10">
-    <uid>location%02d</uid>
-    <enable>true</enable>
+</record>
+<record>
+    <uid>Default-Location</uid>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-  </record>
-  <record repeat="4">
-    <uid>resource%02d</uid>
-    <enable>true</enable>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+</record>
+<record>
+    <uid>Default-Resource</uid>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-  </record>
-  <record>
-    <uid>resource05</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000005</uid>
     <auto-schedule-mode>none</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>resource06</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>accept-always</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>resource07</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000006</uid>
+    <auto-schedule-mode>accept-always</auto-schedule-mode>
     <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>decline-always</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>resource08</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>accept-if-free</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>resource09</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>decline-if-busy</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>resource10</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>automatic</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>resource11</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <auto-schedule>true</auto-schedule>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000007</uid>
     <auto-schedule-mode>decline-always</auto-schedule-mode>
-    <auto-accept-group>group01</auto-accept-group>
-  </record>
-  <record repeat="10">
-    <uid>group%02d</uid>
-    <enable>true</enable>
-  </record>
-  <record>
-    <uid>disabledgroup</uid>
-    <enable>false</enable>
-  </record>
-  <record>
-    <uid>delegatedroom</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>false</enable-addressbook>
-    <auto-schedule>false</auto-schedule>
-  </record>
-  <record>
-    <uid>03DFF660-8BCC-4198-8588-DD77F776F518</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-  </record>
-  <record>
-    <uid>80689D41-DAF8-4189-909C-DB017B271892</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000008</uid>
+    <auto-schedule-mode>accept-if-free</auto-schedule-mode>
     <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000009</uid>
+    <auto-schedule-mode>decline-if-busy</auto-schedule-mode>
     <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-    <auto-accept-group>group01</auto-accept-group>
-  </record>
-  <record>
-    <uid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000010</uid>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
     <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-  </record>
-  <record>
-    <uid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
+</record>
+<record>
+    <uid>40000000-0000-0000-0000-000000000011</uid>
+    <auto-schedule-mode>decline-always</auto-schedule-mode>
     <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-  </record>
-  <record>
-    <uid>6F9EE33B-78F6-481B-9289-3D0812FF0D64</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>false</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>false</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>false</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>06E3BDCB-9C19-485A-B14E-F146A80ADDC6</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-  </record>
-  <record>
-    <uid>4D66A20A-1437-437D-8069-2F14E8322234</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <enable-addressbook>true</enable-addressbook>
-    <enable-login>true</enable-login>
-    <auto-schedule>true</auto-schedule>
-    <auto-schedule-mode>default</auto-schedule-mode>
-  </record>
+    <auto-accept-group>20000000-0000-0000-0000-000000000001</auto-accept-group>
+</record>
 </augments>

Copied: CalendarServer/branches/users/sagen/move2who-5/conf/auth/generate_test_accounts.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/conf/auth/generate_test_accounts.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/conf/auth/generate_test_accounts.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/conf/auth/generate_test_accounts.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,341 @@
+#!/usr/bin/env python
+
+# Generates test directory records in accounts-test.xml, resources-test.xml,
+# augments-test.xml and proxies-test.xml (overwriting them if they exist in
+# the current directory).
+
+prefix = """<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2014 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.
+ -->
+
+"""
+
+# The uids and guids for CDT test accounts are the same
+# The short name is of the form userNN
+USERGUIDS = "10000000-0000-0000-0000-000000000%03d"
+GROUPGUIDS = "20000000-0000-0000-0000-000000000%03d"
+LOCATIONGUIDS = "30000000-0000-0000-0000-000000000%03d"
+RESOURCEGUIDS = "40000000-0000-0000-0000-000000000%03d"
+PUBLICGUIDS = "50000000-0000-0000-0000-0000000000%02d"
+
+# accounts-test.xml
+
+out = file("accounts-test.xml", "w")
+out.write(prefix)
+out.write('<directory realm="Test Realm">\n')
+
+
+for uid, fullName, guid in (
+    ("admin", "Super User", "0C8BDE62-E600-4696-83D3-8B5ECABDFD2E"),
+    ("apprentice", "Apprentice Super User", "29B6C503-11DF-43EC-8CCA-40C7003149CE"),
+    ("i18nuser", u"\u307e\u3060".encode("utf-8"), "860B3EE9-6D7C-4296-9639-E6B998074A78"),
+):
+    out.write("""<record>
+    <uid>{guid}</uid>
+    <guid>{guid}</guid>
+    <short-name>{uid}</short-name>
+    <password>{uid}</password>
+    <full-name>{fullName}</full-name>
+    <email>{uid}@example.com</email>
+</record>
+""".format(uid=uid, guid=guid, fullName=fullName))
+
+# user01-100
+for i in xrange(1, 101):
+    out.write("""<record type="user">
+    <short-name>user%02d</short-name>
+    <uid>%s</uid>
+    <guid>%s</guid>
+    <password>user%02d</password>
+    <full-name>User %02d</full-name>
+    <email>user%02d at example.com</email>
+</record>
+""" % (i, USERGUIDS % i, USERGUIDS % i, i, i, i))
+
+# public01-10
+for i in xrange(1, 11):
+    out.write("""<record type="user">
+    <short-name>public%02d</short-name>
+    <uid>%s</uid>
+    <guid>%s</guid>
+    <password>public%02d</password>
+    <full-name>Public %02d</full-name>
+    <email>public%02d at example.com</email>
+</record>
+""" % (i, PUBLICGUIDS % i, PUBLICGUIDS % i, i, i, i))
+
+# group01-100
+members = {
+    GROUPGUIDS % 1: (USERGUIDS % 1,),
+    GROUPGUIDS % 2: (USERGUIDS % 6, USERGUIDS % 7),
+    GROUPGUIDS % 3: (USERGUIDS % 8, USERGUIDS % 9),
+    GROUPGUIDS % 4: (GROUPGUIDS % 2, GROUPGUIDS % 3, USERGUIDS % 10),
+    GROUPGUIDS % 5: (GROUPGUIDS % 6, USERGUIDS % 20),
+    GROUPGUIDS % 6: (USERGUIDS % 21,),
+    GROUPGUIDS % 7: (USERGUIDS % 22, USERGUIDS % 23, USERGUIDS % 24),
+}
+
+for i in xrange(1, 101):
+
+    memberElements = []
+    groupUID = GROUPGUIDS % i
+    if groupUID in members:
+        for uid in members[groupUID]:
+            memberElements.append("<member-uid>{}</member-uid>".format(uid))
+        memberString = "\n    ".join(memberElements)
+    else:
+        memberString = ""
+
+    out.write("""<record type="group">
+    <short-name>group%02d</short-name>
+    <uid>%s</uid>
+    <guid>%s</guid>
+    <full-name>Group %02d</full-name>
+    <email>group%02d at example.com</email>
+    %s
+</record>
+""" % (i, GROUPGUIDS % i, GROUPGUIDS % i, i, i, memberString))
+
+out.write("</directory>\n")
+out.close()
+
+
+# resources-test.xml
+
+out = file("resources-test.xml", "w")
+out.write(prefix)
+out.write('<directory realm="Test Realm">\n')
+
+out.write("""
+  <record type="location">
+    <short-name>pretend</short-name>
+    <uid>pretend</uid>
+    <full-name>Pretend Conference Room</full-name>
+    <associated-address>il1</associated-address>
+  </record>
+  <record type="address">
+    <short-name>il1</short-name>
+    <uid>il1</uid>
+    <full-name>IL1</full-name>
+    <street-address>1 Infinite Loop, Cupertino, CA 95014</street-address>
+    <geographic-location>37.331741,-122.030333</geographic-location>
+  </record>
+  <record type="location">
+    <short-name>fantastic</short-name>
+    <uid>fantastic</uid>
+    <full-name>Fantastic Conference Room</full-name>
+    <associated-address>il2</associated-address>
+  </record>
+  <record type="address">
+    <short-name>il2</short-name>
+    <uid>il2</uid>
+    <full-name>IL2</full-name>
+    <street-address>2 Infinite Loop, Cupertino, CA 95014</street-address>
+    <geographic-location>37.332633,-122.030502</geographic-location>
+  </record>
+  <record type="location">
+    <short-name>delegatedroom</short-name>
+    <uid>delegatedroom</uid>
+    <full-name>Delegated Conference Room</full-name>
+  </record>
+
+""")
+
+for i in xrange(1, 101):
+    out.write("""<record type="location">
+    <short-name>location%02d</short-name>
+    <uid>%s</uid>
+    <guid>%s</guid>
+    <full-name>Location %02d</full-name>
+</record>
+""" % (i, LOCATIONGUIDS % i, LOCATIONGUIDS % i, i))
+
+
+for i in xrange(1, 101):
+    out.write("""<record type="resource">
+    <short-name>resource%02d</short-name>
+    <uid>%s</uid>
+    <guid>%s</guid>
+    <full-name>Resource %02d</full-name>
+</record>
+""" % (i, RESOURCEGUIDS % i, RESOURCEGUIDS % i, i))
+
+out.write("</directory>\n")
+out.close()
+
+
+# augments-test.xml
+
+out = file("augments-test.xml", "w")
+out.write(prefix)
+out.write("<augments>\n")
+
+augments = (
+    # resource05
+    (RESOURCEGUIDS % 5, {
+        "auto-schedule-mode": "none",
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+    # resource06
+    (RESOURCEGUIDS % 6, {
+        "auto-schedule-mode": "accept-always",
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+    # resource07
+    (RESOURCEGUIDS % 7, {
+        "auto-schedule-mode": "decline-always",
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+    # resource08
+    (RESOURCEGUIDS % 8, {
+        "auto-schedule-mode": "accept-if-free",
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+    # resource09
+    (RESOURCEGUIDS % 9, {
+        "auto-schedule-mode": "decline-if-busy",
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+    # resource10
+    (RESOURCEGUIDS % 10, {
+        "auto-schedule-mode": "automatic",
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+    # resource11
+    (RESOURCEGUIDS % 11, {
+        "auto-schedule-mode": "decline-always",
+        "auto-accept-group": GROUPGUIDS % 1,
+        "enable-calendar": "true",
+        "enable-addressbook": "true",
+    }),
+)
+
+out.write("""<record>
+    <uid>Default</uid>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+</record>
+""")
+
+out.write("""<record>
+    <uid>Default-Location</uid>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+</record>
+""")
+
+out.write("""<record>
+    <uid>Default-Resource</uid>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+</record>
+""")
+
+for uid, settings in augments:
+    elements = []
+    for key, value in settings.iteritems():
+        elements.append("<{key}>{value}</{key}>".format(key=key, value=value))
+    elementsString = "\n    ".join(elements)
+
+    out.write("""<record>
+    <uid>{uid}</uid>
+    {elements}
+</record>
+""".format(uid=uid, elements=elementsString))
+
+out.write("</augments>\n")
+out.close()
+
+
+# proxies-test.xml
+
+out = file("proxies-test.xml", "w")
+out.write(prefix)
+out.write("<proxies>\n")
+
+proxies = (
+    (RESOURCEGUIDS % 1, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 2, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 3, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 4, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 5, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 6, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 7, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 8, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 9, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    (RESOURCEGUIDS % 10, {
+        "proxies": (USERGUIDS % 1,),
+        "read-only-proxies": (USERGUIDS % 3,),
+    }),
+    ("delegatedroom", {
+        "proxies": (GROUPGUIDS % 5,),
+        "read-only-proxies": (),
+    }),
+)
+
+for uid, settings in proxies:
+    elements = []
+    for key, values in settings.iteritems():
+        elements.append("<{key}>".format(key=key))
+        for value in values:
+            elements.append("<member>{value}</member>".format(value=value))
+        elements.append("</{key}>".format(key=key))
+    elementsString = "\n    ".join(elements)
+
+    out.write("""<record>
+    <guid>{uid}</guid>
+    {elements}
+</record>
+""".format(uid=uid, elements=elementsString))
+
+out.write("</proxies>\n")
+out.close()

Modified: CalendarServer/branches/users/sagen/move2who-5/conf/auth/proxies-test.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/conf/auth/proxies-test.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/conf/auth/proxies-test.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 
 <!--
-Copyright (c) 2009-2014 Apple Inc. All rights reserved.
+Copyright (c) 2006-2014 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,25 +16,103 @@
 limitations under the License.
  -->
 
-<!DOCTYPE proxies SYSTEM "proxies.dtd">
-
 <proxies>
-  <record repeat="10">
-    <guid>resource%02d</guid>
+<record>
+    <guid>40000000-0000-0000-0000-000000000001</guid>
     <proxies>
-      <member>user01</member>
+    <member>10000000-0000-0000-0000-000000000001</member>
     </proxies>
     <read-only-proxies>
-      <member>user03</member>
+    <member>10000000-0000-0000-0000-000000000003</member>
     </read-only-proxies>
-  </record>
-  <record>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000002</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000003</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000004</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000005</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000006</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000007</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000008</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000009</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
+    <guid>40000000-0000-0000-0000-000000000010</guid>
+    <proxies>
+    <member>10000000-0000-0000-0000-000000000001</member>
+    </proxies>
+    <read-only-proxies>
+    <member>10000000-0000-0000-0000-000000000003</member>
+    </read-only-proxies>
+</record>
+<record>
     <guid>delegatedroom</guid>
     <proxies>
-      <member>group05</member>
+    <member>20000000-0000-0000-0000-000000000005</member>
     </proxies>
     <read-only-proxies>
-      <member>group07</member>
     </read-only-proxies>
-  </record>
+</record>
 </proxies>

Modified: CalendarServer/branches/users/sagen/move2who-5/conf/auth/resources-test.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/conf/auth/resources-test.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/conf/auth/resources-test.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,135 +1,1253 @@
-<accounts realm="Test Realm">
-  <location repeat="10">
-    <uid>location%02d</uid>
-    <guid>location%02d</guid>
-    <password>location%02d</password>
-    <name>Room %02d</name>
-  </location>
-  <resource repeat="20">
-    <uid>resource%02d</uid>
-    <guid>resource%02d</guid>
-    <password>resource%02d</password>
-    <name>Resource %02d</name>
-  </resource>
-  <location>
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2014 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.
+ -->
+
+<directory realm="Test Realm">
+
+  <record type="location">
+    <short-name>pretend</short-name>
+    <uid>pretend</uid>
+    <full-name>Pretend Conference Room</full-name>
+    <associated-address>il1</associated-address>
+  </record>
+  <record type="address">
+    <short-name>il1</short-name>
+    <uid>il1</uid>
+    <full-name>IL1</full-name>
+    <street-address>1 Infinite Loop, Cupertino, CA 95014</street-address>
+    <geographic-location>37.331741,-122.030333</geographic-location>
+  </record>
+  <record type="location">
+    <short-name>fantastic</short-name>
     <uid>fantastic</uid>
-    <guid>4D66A20A-1437-437D-8069-2F14E8322234</guid>
-    <name>Fantastic Conference Room</name>
-    <extras>
-      <associatedAddress>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</associatedAddress>
-    </extras>
-  </location>
-  <location>
-    <uid>jupiter</uid>
-    <guid>jupiter</guid>
-    <name>Jupiter Conference Room, Building 2, 1st Floor</name>
-  </location>
-  <location>
-    <uid>uranus</uid>
-    <guid>uranus</guid>
-    <name>Uranus Conference Room, Building 3, 1st Floor</name>
-  </location>
-  <location>
-    <uid>morgensroom</uid>
-    <guid>03DFF660-8BCC-4198-8588-DD77F776F518</guid>
-    <name>Morgen's Room</name>
-  </location>
-  <location>
-    <uid>mercury</uid>
-    <guid>mercury</guid>
-    <name>Mercury Conference Room, Building 1, 2nd Floor</name>
-  </location>
-  <location>
+    <full-name>Fantastic Conference Room</full-name>
+    <associated-address>il2</associated-address>
+  </record>
+  <record type="address">
+    <short-name>il2</short-name>
+    <uid>il2</uid>
+    <full-name>IL2</full-name>
+    <street-address>2 Infinite Loop, Cupertino, CA 95014</street-address>
+    <geographic-location>37.332633,-122.030502</geographic-location>
+  </record>
+  <record type="location">
+    <short-name>delegatedroom</short-name>
     <uid>delegatedroom</uid>
-    <guid>delegatedroom</guid>
-    <name>Delegated Conference Room</name>
-  </location>
-  <location>
-    <uid>mars</uid>
-    <guid>redplanet</guid>
-    <name>Mars Conference Room, Building 1, 1st Floor</name>
-  </location>
-  <location>
-    <uid>sharissroom</uid>
-    <guid>80689D41-DAF8-4189-909C-DB017B271892</guid>
-    <name>Shari's Room</name>
-    <extras>
-      <associatedAddress>6F9EE33B-78F6-481B-9289-3D0812FF0D64</associatedAddress>
-    </extras>
-  </location>
-  <location>
-    <uid>pluto</uid>
-    <guid>pluto</guid>
-    <name>Pluto Conference Room, Building 2, 1st Floor</name>
-  </location>
-  <location>
-    <uid>saturn</uid>
-    <guid>saturn</guid>
-    <name>Saturn Conference Room, Building 2, 1st Floor</name>
-  </location>
-  <location>
-    <uid>pretend</uid>
-    <guid>06E3BDCB-9C19-485A-B14E-F146A80ADDC6</guid>
-    <name>Pretend Conference Room</name>
-    <extras>
-      <associatedAddress>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</associatedAddress>
-    </extras>
-  </location>
-  <location>
-    <uid>neptune</uid>
-    <guid>neptune</guid>
-    <name>Neptune Conference Room, Building 2, 1st Floor</name>
-  </location>
-  <location>
-    <uid>Earth</uid>
-    <guid>Earth</guid>
-    <name>Earth Conference Room, Building 1, 1st Floor</name>
-  </location>
-  <location>
-    <uid>venus</uid>
-    <guid>venus</guid>
-    <name>Venus Conference Room, Building 1, 2nd Floor</name>
-  </location>
-  <resource>
-    <uid>sharisotherresource</uid>
-    <guid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</guid>
-    <name>Shari's Other Resource</name>
-  </resource>
-  <resource>
-    <uid>sharisresource</uid>
-    <guid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</guid>
-    <name>Shari's Resource</name>
-  </resource>
-  <resource>
-    <uid>sharisotherresource1</uid>
-    <guid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</guid>
-    <name>Shari's Other Resource1</name>
-  </resource>
-  <address>
-    <uid>testaddress1</uid>
-    <guid>6F9EE33B-78F6-481B-9289-3D0812FF0D64</guid>
-    <name>Test Address One</name>
-    <extras>
-      <streetAddress>20300 Stevens Creek Blvd, Cupertino, CA 95014</streetAddress>
-      <geo>37.322281,-122.028345</geo>
-    </extras>
-  </address>
-  <address>
-    <uid>il2</uid>
-    <guid>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</guid>
-    <name>IL2</name>
-    <extras>
-      <streetAddress>2 Infinite Loop, Cupertino, CA 95014</streetAddress>
-      <geo>37.332633,-122.030502</geo>
-    </extras>
-  </address>
-  <address>
-    <uid>il1</uid>
-    <guid>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</guid>
-    <name>IL1</name>
-    <extras>
-      <streetAddress>1 Infinite Loop, Cupertino, CA 95014</streetAddress>
-      <geo>37.331741,-122.030333</geo>
-    </extras>
-  </address>
-</accounts>
+    <full-name>Delegated Conference Room</full-name>
+  </record>
+
+<record type="location">
+    <short-name>location01</short-name>
+    <uid>30000000-0000-0000-0000-000000000001</uid>
+    <guid>30000000-0000-0000-0000-000000000001</guid>
+    <full-name>Location 01</full-name>
+</record>
+<record type="location">
+    <short-name>location02</short-name>
+    <uid>30000000-0000-0000-0000-000000000002</uid>
+    <guid>30000000-0000-0000-0000-000000000002</guid>
+    <full-name>Location 02</full-name>
+</record>
+<record type="location">
+    <short-name>location03</short-name>
+    <uid>30000000-0000-0000-0000-000000000003</uid>
+    <guid>30000000-0000-0000-0000-000000000003</guid>
+    <full-name>Location 03</full-name>
+</record>
+<record type="location">
+    <short-name>location04</short-name>
+    <uid>30000000-0000-0000-0000-000000000004</uid>
+    <guid>30000000-0000-0000-0000-000000000004</guid>
+    <full-name>Location 04</full-name>
+</record>
+<record type="location">
+    <short-name>location05</short-name>
+    <uid>30000000-0000-0000-0000-000000000005</uid>
+    <guid>30000000-0000-0000-0000-000000000005</guid>
+    <full-name>Location 05</full-name>
+</record>
+<record type="location">
+    <short-name>location06</short-name>
+    <uid>30000000-0000-0000-0000-000000000006</uid>
+    <guid>30000000-0000-0000-0000-000000000006</guid>
+    <full-name>Location 06</full-name>
+</record>
+<record type="location">
+    <short-name>location07</short-name>
+    <uid>30000000-0000-0000-0000-000000000007</uid>
+    <guid>30000000-0000-0000-0000-000000000007</guid>
+    <full-name>Location 07</full-name>
+</record>
+<record type="location">
+    <short-name>location08</short-name>
+    <uid>30000000-0000-0000-0000-000000000008</uid>
+    <guid>30000000-0000-0000-0000-000000000008</guid>
+    <full-name>Location 08</full-name>
+</record>
+<record type="location">
+    <short-name>location09</short-name>
+    <uid>30000000-0000-0000-0000-000000000009</uid>
+    <guid>30000000-0000-0000-0000-000000000009</guid>
+    <full-name>Location 09</full-name>
+</record>
+<record type="location">
+    <short-name>location10</short-name>
+    <uid>30000000-0000-0000-0000-000000000010</uid>
+    <guid>30000000-0000-0000-0000-000000000010</guid>
+    <full-name>Location 10</full-name>
+</record>
+<record type="location">
+    <short-name>location11</short-name>
+    <uid>30000000-0000-0000-0000-000000000011</uid>
+    <guid>30000000-0000-0000-0000-000000000011</guid>
+    <full-name>Location 11</full-name>
+</record>
+<record type="location">
+    <short-name>location12</short-name>
+    <uid>30000000-0000-0000-0000-000000000012</uid>
+    <guid>30000000-0000-0000-0000-000000000012</guid>
+    <full-name>Location 12</full-name>
+</record>
+<record type="location">
+    <short-name>location13</short-name>
+    <uid>30000000-0000-0000-0000-000000000013</uid>
+    <guid>30000000-0000-0000-0000-000000000013</guid>
+    <full-name>Location 13</full-name>
+</record>
+<record type="location">
+    <short-name>location14</short-name>
+    <uid>30000000-0000-0000-0000-000000000014</uid>
+    <guid>30000000-0000-0000-0000-000000000014</guid>
+    <full-name>Location 14</full-name>
+</record>
+<record type="location">
+    <short-name>location15</short-name>
+    <uid>30000000-0000-0000-0000-000000000015</uid>
+    <guid>30000000-0000-0000-0000-000000000015</guid>
+    <full-name>Location 15</full-name>
+</record>
+<record type="location">
+    <short-name>location16</short-name>
+    <uid>30000000-0000-0000-0000-000000000016</uid>
+    <guid>30000000-0000-0000-0000-000000000016</guid>
+    <full-name>Location 16</full-name>
+</record>
+<record type="location">
+    <short-name>location17</short-name>
+    <uid>30000000-0000-0000-0000-000000000017</uid>
+    <guid>30000000-0000-0000-0000-000000000017</guid>
+    <full-name>Location 17</full-name>
+</record>
+<record type="location">
+    <short-name>location18</short-name>
+    <uid>30000000-0000-0000-0000-000000000018</uid>
+    <guid>30000000-0000-0000-0000-000000000018</guid>
+    <full-name>Location 18</full-name>
+</record>
+<record type="location">
+    <short-name>location19</short-name>
+    <uid>30000000-0000-0000-0000-000000000019</uid>
+    <guid>30000000-0000-0000-0000-000000000019</guid>
+    <full-name>Location 19</full-name>
+</record>
+<record type="location">
+    <short-name>location20</short-name>
+    <uid>30000000-0000-0000-0000-000000000020</uid>
+    <guid>30000000-0000-0000-0000-000000000020</guid>
+    <full-name>Location 20</full-name>
+</record>
+<record type="location">
+    <short-name>location21</short-name>
+    <uid>30000000-0000-0000-0000-000000000021</uid>
+    <guid>30000000-0000-0000-0000-000000000021</guid>
+    <full-name>Location 21</full-name>
+</record>
+<record type="location">
+    <short-name>location22</short-name>
+    <uid>30000000-0000-0000-0000-000000000022</uid>
+    <guid>30000000-0000-0000-0000-000000000022</guid>
+    <full-name>Location 22</full-name>
+</record>
+<record type="location">
+    <short-name>location23</short-name>
+    <uid>30000000-0000-0000-0000-000000000023</uid>
+    <guid>30000000-0000-0000-0000-000000000023</guid>
+    <full-name>Location 23</full-name>
+</record>
+<record type="location">
+    <short-name>location24</short-name>
+    <uid>30000000-0000-0000-0000-000000000024</uid>
+    <guid>30000000-0000-0000-0000-000000000024</guid>
+    <full-name>Location 24</full-name>
+</record>
+<record type="location">
+    <short-name>location25</short-name>
+    <uid>30000000-0000-0000-0000-000000000025</uid>
+    <guid>30000000-0000-0000-0000-000000000025</guid>
+    <full-name>Location 25</full-name>
+</record>
+<record type="location">
+    <short-name>location26</short-name>
+    <uid>30000000-0000-0000-0000-000000000026</uid>
+    <guid>30000000-0000-0000-0000-000000000026</guid>
+    <full-name>Location 26</full-name>
+</record>
+<record type="location">
+    <short-name>location27</short-name>
+    <uid>30000000-0000-0000-0000-000000000027</uid>
+    <guid>30000000-0000-0000-0000-000000000027</guid>
+    <full-name>Location 27</full-name>
+</record>
+<record type="location">
+    <short-name>location28</short-name>
+    <uid>30000000-0000-0000-0000-000000000028</uid>
+    <guid>30000000-0000-0000-0000-000000000028</guid>
+    <full-name>Location 28</full-name>
+</record>
+<record type="location">
+    <short-name>location29</short-name>
+    <uid>30000000-0000-0000-0000-000000000029</uid>
+    <guid>30000000-0000-0000-0000-000000000029</guid>
+    <full-name>Location 29</full-name>
+</record>
+<record type="location">
+    <short-name>location30</short-name>
+    <uid>30000000-0000-0000-0000-000000000030</uid>
+    <guid>30000000-0000-0000-0000-000000000030</guid>
+    <full-name>Location 30</full-name>
+</record>
+<record type="location">
+    <short-name>location31</short-name>
+    <uid>30000000-0000-0000-0000-000000000031</uid>
+    <guid>30000000-0000-0000-0000-000000000031</guid>
+    <full-name>Location 31</full-name>
+</record>
+<record type="location">
+    <short-name>location32</short-name>
+    <uid>30000000-0000-0000-0000-000000000032</uid>
+    <guid>30000000-0000-0000-0000-000000000032</guid>
+    <full-name>Location 32</full-name>
+</record>
+<record type="location">
+    <short-name>location33</short-name>
+    <uid>30000000-0000-0000-0000-000000000033</uid>
+    <guid>30000000-0000-0000-0000-000000000033</guid>
+    <full-name>Location 33</full-name>
+</record>
+<record type="location">
+    <short-name>location34</short-name>
+    <uid>30000000-0000-0000-0000-000000000034</uid>
+    <guid>30000000-0000-0000-0000-000000000034</guid>
+    <full-name>Location 34</full-name>
+</record>
+<record type="location">
+    <short-name>location35</short-name>
+    <uid>30000000-0000-0000-0000-000000000035</uid>
+    <guid>30000000-0000-0000-0000-000000000035</guid>
+    <full-name>Location 35</full-name>
+</record>
+<record type="location">
+    <short-name>location36</short-name>
+    <uid>30000000-0000-0000-0000-000000000036</uid>
+    <guid>30000000-0000-0000-0000-000000000036</guid>
+    <full-name>Location 36</full-name>
+</record>
+<record type="location">
+    <short-name>location37</short-name>
+    <uid>30000000-0000-0000-0000-000000000037</uid>
+    <guid>30000000-0000-0000-0000-000000000037</guid>
+    <full-name>Location 37</full-name>
+</record>
+<record type="location">
+    <short-name>location38</short-name>
+    <uid>30000000-0000-0000-0000-000000000038</uid>
+    <guid>30000000-0000-0000-0000-000000000038</guid>
+    <full-name>Location 38</full-name>
+</record>
+<record type="location">
+    <short-name>location39</short-name>
+    <uid>30000000-0000-0000-0000-000000000039</uid>
+    <guid>30000000-0000-0000-0000-000000000039</guid>
+    <full-name>Location 39</full-name>
+</record>
+<record type="location">
+    <short-name>location40</short-name>
+    <uid>30000000-0000-0000-0000-000000000040</uid>
+    <guid>30000000-0000-0000-0000-000000000040</guid>
+    <full-name>Location 40</full-name>
+</record>
+<record type="location">
+    <short-name>location41</short-name>
+    <uid>30000000-0000-0000-0000-000000000041</uid>
+    <guid>30000000-0000-0000-0000-000000000041</guid>
+    <full-name>Location 41</full-name>
+</record>
+<record type="location">
+    <short-name>location42</short-name>
+    <uid>30000000-0000-0000-0000-000000000042</uid>
+    <guid>30000000-0000-0000-0000-000000000042</guid>
+    <full-name>Location 42</full-name>
+</record>
+<record type="location">
+    <short-name>location43</short-name>
+    <uid>30000000-0000-0000-0000-000000000043</uid>
+    <guid>30000000-0000-0000-0000-000000000043</guid>
+    <full-name>Location 43</full-name>
+</record>
+<record type="location">
+    <short-name>location44</short-name>
+    <uid>30000000-0000-0000-0000-000000000044</uid>
+    <guid>30000000-0000-0000-0000-000000000044</guid>
+    <full-name>Location 44</full-name>
+</record>
+<record type="location">
+    <short-name>location45</short-name>
+    <uid>30000000-0000-0000-0000-000000000045</uid>
+    <guid>30000000-0000-0000-0000-000000000045</guid>
+    <full-name>Location 45</full-name>
+</record>
+<record type="location">
+    <short-name>location46</short-name>
+    <uid>30000000-0000-0000-0000-000000000046</uid>
+    <guid>30000000-0000-0000-0000-000000000046</guid>
+    <full-name>Location 46</full-name>
+</record>
+<record type="location">
+    <short-name>location47</short-name>
+    <uid>30000000-0000-0000-0000-000000000047</uid>
+    <guid>30000000-0000-0000-0000-000000000047</guid>
+    <full-name>Location 47</full-name>
+</record>
+<record type="location">
+    <short-name>location48</short-name>
+    <uid>30000000-0000-0000-0000-000000000048</uid>
+    <guid>30000000-0000-0000-0000-000000000048</guid>
+    <full-name>Location 48</full-name>
+</record>
+<record type="location">
+    <short-name>location49</short-name>
+    <uid>30000000-0000-0000-0000-000000000049</uid>
+    <guid>30000000-0000-0000-0000-000000000049</guid>
+    <full-name>Location 49</full-name>
+</record>
+<record type="location">
+    <short-name>location50</short-name>
+    <uid>30000000-0000-0000-0000-000000000050</uid>
+    <guid>30000000-0000-0000-0000-000000000050</guid>
+    <full-name>Location 50</full-name>
+</record>
+<record type="location">
+    <short-name>location51</short-name>
+    <uid>30000000-0000-0000-0000-000000000051</uid>
+    <guid>30000000-0000-0000-0000-000000000051</guid>
+    <full-name>Location 51</full-name>
+</record>
+<record type="location">
+    <short-name>location52</short-name>
+    <uid>30000000-0000-0000-0000-000000000052</uid>
+    <guid>30000000-0000-0000-0000-000000000052</guid>
+    <full-name>Location 52</full-name>
+</record>
+<record type="location">
+    <short-name>location53</short-name>
+    <uid>30000000-0000-0000-0000-000000000053</uid>
+    <guid>30000000-0000-0000-0000-000000000053</guid>
+    <full-name>Location 53</full-name>
+</record>
+<record type="location">
+    <short-name>location54</short-name>
+    <uid>30000000-0000-0000-0000-000000000054</uid>
+    <guid>30000000-0000-0000-0000-000000000054</guid>
+    <full-name>Location 54</full-name>
+</record>
+<record type="location">
+    <short-name>location55</short-name>
+    <uid>30000000-0000-0000-0000-000000000055</uid>
+    <guid>30000000-0000-0000-0000-000000000055</guid>
+    <full-name>Location 55</full-name>
+</record>
+<record type="location">
+    <short-name>location56</short-name>
+    <uid>30000000-0000-0000-0000-000000000056</uid>
+    <guid>30000000-0000-0000-0000-000000000056</guid>
+    <full-name>Location 56</full-name>
+</record>
+<record type="location">
+    <short-name>location57</short-name>
+    <uid>30000000-0000-0000-0000-000000000057</uid>
+    <guid>30000000-0000-0000-0000-000000000057</guid>
+    <full-name>Location 57</full-name>
+</record>
+<record type="location">
+    <short-name>location58</short-name>
+    <uid>30000000-0000-0000-0000-000000000058</uid>
+    <guid>30000000-0000-0000-0000-000000000058</guid>
+    <full-name>Location 58</full-name>
+</record>
+<record type="location">
+    <short-name>location59</short-name>
+    <uid>30000000-0000-0000-0000-000000000059</uid>
+    <guid>30000000-0000-0000-0000-000000000059</guid>
+    <full-name>Location 59</full-name>
+</record>
+<record type="location">
+    <short-name>location60</short-name>
+    <uid>30000000-0000-0000-0000-000000000060</uid>
+    <guid>30000000-0000-0000-0000-000000000060</guid>
+    <full-name>Location 60</full-name>
+</record>
+<record type="location">
+    <short-name>location61</short-name>
+    <uid>30000000-0000-0000-0000-000000000061</uid>
+    <guid>30000000-0000-0000-0000-000000000061</guid>
+    <full-name>Location 61</full-name>
+</record>
+<record type="location">
+    <short-name>location62</short-name>
+    <uid>30000000-0000-0000-0000-000000000062</uid>
+    <guid>30000000-0000-0000-0000-000000000062</guid>
+    <full-name>Location 62</full-name>
+</record>
+<record type="location">
+    <short-name>location63</short-name>
+    <uid>30000000-0000-0000-0000-000000000063</uid>
+    <guid>30000000-0000-0000-0000-000000000063</guid>
+    <full-name>Location 63</full-name>
+</record>
+<record type="location">
+    <short-name>location64</short-name>
+    <uid>30000000-0000-0000-0000-000000000064</uid>
+    <guid>30000000-0000-0000-0000-000000000064</guid>
+    <full-name>Location 64</full-name>
+</record>
+<record type="location">
+    <short-name>location65</short-name>
+    <uid>30000000-0000-0000-0000-000000000065</uid>
+    <guid>30000000-0000-0000-0000-000000000065</guid>
+    <full-name>Location 65</full-name>
+</record>
+<record type="location">
+    <short-name>location66</short-name>
+    <uid>30000000-0000-0000-0000-000000000066</uid>
+    <guid>30000000-0000-0000-0000-000000000066</guid>
+    <full-name>Location 66</full-name>
+</record>
+<record type="location">
+    <short-name>location67</short-name>
+    <uid>30000000-0000-0000-0000-000000000067</uid>
+    <guid>30000000-0000-0000-0000-000000000067</guid>
+    <full-name>Location 67</full-name>
+</record>
+<record type="location">
+    <short-name>location68</short-name>
+    <uid>30000000-0000-0000-0000-000000000068</uid>
+    <guid>30000000-0000-0000-0000-000000000068</guid>
+    <full-name>Location 68</full-name>
+</record>
+<record type="location">
+    <short-name>location69</short-name>
+    <uid>30000000-0000-0000-0000-000000000069</uid>
+    <guid>30000000-0000-0000-0000-000000000069</guid>
+    <full-name>Location 69</full-name>
+</record>
+<record type="location">
+    <short-name>location70</short-name>
+    <uid>30000000-0000-0000-0000-000000000070</uid>
+    <guid>30000000-0000-0000-0000-000000000070</guid>
+    <full-name>Location 70</full-name>
+</record>
+<record type="location">
+    <short-name>location71</short-name>
+    <uid>30000000-0000-0000-0000-000000000071</uid>
+    <guid>30000000-0000-0000-0000-000000000071</guid>
+    <full-name>Location 71</full-name>
+</record>
+<record type="location">
+    <short-name>location72</short-name>
+    <uid>30000000-0000-0000-0000-000000000072</uid>
+    <guid>30000000-0000-0000-0000-000000000072</guid>
+    <full-name>Location 72</full-name>
+</record>
+<record type="location">
+    <short-name>location73</short-name>
+    <uid>30000000-0000-0000-0000-000000000073</uid>
+    <guid>30000000-0000-0000-0000-000000000073</guid>
+    <full-name>Location 73</full-name>
+</record>
+<record type="location">
+    <short-name>location74</short-name>
+    <uid>30000000-0000-0000-0000-000000000074</uid>
+    <guid>30000000-0000-0000-0000-000000000074</guid>
+    <full-name>Location 74</full-name>
+</record>
+<record type="location">
+    <short-name>location75</short-name>
+    <uid>30000000-0000-0000-0000-000000000075</uid>
+    <guid>30000000-0000-0000-0000-000000000075</guid>
+    <full-name>Location 75</full-name>
+</record>
+<record type="location">
+    <short-name>location76</short-name>
+    <uid>30000000-0000-0000-0000-000000000076</uid>
+    <guid>30000000-0000-0000-0000-000000000076</guid>
+    <full-name>Location 76</full-name>
+</record>
+<record type="location">
+    <short-name>location77</short-name>
+    <uid>30000000-0000-0000-0000-000000000077</uid>
+    <guid>30000000-0000-0000-0000-000000000077</guid>
+    <full-name>Location 77</full-name>
+</record>
+<record type="location">
+    <short-name>location78</short-name>
+    <uid>30000000-0000-0000-0000-000000000078</uid>
+    <guid>30000000-0000-0000-0000-000000000078</guid>
+    <full-name>Location 78</full-name>
+</record>
+<record type="location">
+    <short-name>location79</short-name>
+    <uid>30000000-0000-0000-0000-000000000079</uid>
+    <guid>30000000-0000-0000-0000-000000000079</guid>
+    <full-name>Location 79</full-name>
+</record>
+<record type="location">
+    <short-name>location80</short-name>
+    <uid>30000000-0000-0000-0000-000000000080</uid>
+    <guid>30000000-0000-0000-0000-000000000080</guid>
+    <full-name>Location 80</full-name>
+</record>
+<record type="location">
+    <short-name>location81</short-name>
+    <uid>30000000-0000-0000-0000-000000000081</uid>
+    <guid>30000000-0000-0000-0000-000000000081</guid>
+    <full-name>Location 81</full-name>
+</record>
+<record type="location">
+    <short-name>location82</short-name>
+    <uid>30000000-0000-0000-0000-000000000082</uid>
+    <guid>30000000-0000-0000-0000-000000000082</guid>
+    <full-name>Location 82</full-name>
+</record>
+<record type="location">
+    <short-name>location83</short-name>
+    <uid>30000000-0000-0000-0000-000000000083</uid>
+    <guid>30000000-0000-0000-0000-000000000083</guid>
+    <full-name>Location 83</full-name>
+</record>
+<record type="location">
+    <short-name>location84</short-name>
+    <uid>30000000-0000-0000-0000-000000000084</uid>
+    <guid>30000000-0000-0000-0000-000000000084</guid>
+    <full-name>Location 84</full-name>
+</record>
+<record type="location">
+    <short-name>location85</short-name>
+    <uid>30000000-0000-0000-0000-000000000085</uid>
+    <guid>30000000-0000-0000-0000-000000000085</guid>
+    <full-name>Location 85</full-name>
+</record>
+<record type="location">
+    <short-name>location86</short-name>
+    <uid>30000000-0000-0000-0000-000000000086</uid>
+    <guid>30000000-0000-0000-0000-000000000086</guid>
+    <full-name>Location 86</full-name>
+</record>
+<record type="location">
+    <short-name>location87</short-name>
+    <uid>30000000-0000-0000-0000-000000000087</uid>
+    <guid>30000000-0000-0000-0000-000000000087</guid>
+    <full-name>Location 87</full-name>
+</record>
+<record type="location">
+    <short-name>location88</short-name>
+    <uid>30000000-0000-0000-0000-000000000088</uid>
+    <guid>30000000-0000-0000-0000-000000000088</guid>
+    <full-name>Location 88</full-name>
+</record>
+<record type="location">
+    <short-name>location89</short-name>
+    <uid>30000000-0000-0000-0000-000000000089</uid>
+    <guid>30000000-0000-0000-0000-000000000089</guid>
+    <full-name>Location 89</full-name>
+</record>
+<record type="location">
+    <short-name>location90</short-name>
+    <uid>30000000-0000-0000-0000-000000000090</uid>
+    <guid>30000000-0000-0000-0000-000000000090</guid>
+    <full-name>Location 90</full-name>
+</record>
+<record type="location">
+    <short-name>location91</short-name>
+    <uid>30000000-0000-0000-0000-000000000091</uid>
+    <guid>30000000-0000-0000-0000-000000000091</guid>
+    <full-name>Location 91</full-name>
+</record>
+<record type="location">
+    <short-name>location92</short-name>
+    <uid>30000000-0000-0000-0000-000000000092</uid>
+    <guid>30000000-0000-0000-0000-000000000092</guid>
+    <full-name>Location 92</full-name>
+</record>
+<record type="location">
+    <short-name>location93</short-name>
+    <uid>30000000-0000-0000-0000-000000000093</uid>
+    <guid>30000000-0000-0000-0000-000000000093</guid>
+    <full-name>Location 93</full-name>
+</record>
+<record type="location">
+    <short-name>location94</short-name>
+    <uid>30000000-0000-0000-0000-000000000094</uid>
+    <guid>30000000-0000-0000-0000-000000000094</guid>
+    <full-name>Location 94</full-name>
+</record>
+<record type="location">
+    <short-name>location95</short-name>
+    <uid>30000000-0000-0000-0000-000000000095</uid>
+    <guid>30000000-0000-0000-0000-000000000095</guid>
+    <full-name>Location 95</full-name>
+</record>
+<record type="location">
+    <short-name>location96</short-name>
+    <uid>30000000-0000-0000-0000-000000000096</uid>
+    <guid>30000000-0000-0000-0000-000000000096</guid>
+    <full-name>Location 96</full-name>
+</record>
+<record type="location">
+    <short-name>location97</short-name>
+    <uid>30000000-0000-0000-0000-000000000097</uid>
+    <guid>30000000-0000-0000-0000-000000000097</guid>
+    <full-name>Location 97</full-name>
+</record>
+<record type="location">
+    <short-name>location98</short-name>
+    <uid>30000000-0000-0000-0000-000000000098</uid>
+    <guid>30000000-0000-0000-0000-000000000098</guid>
+    <full-name>Location 98</full-name>
+</record>
+<record type="location">
+    <short-name>location99</short-name>
+    <uid>30000000-0000-0000-0000-000000000099</uid>
+    <guid>30000000-0000-0000-0000-000000000099</guid>
+    <full-name>Location 99</full-name>
+</record>
+<record type="location">
+    <short-name>location100</short-name>
+    <uid>30000000-0000-0000-0000-000000000100</uid>
+    <guid>30000000-0000-0000-0000-000000000100</guid>
+    <full-name>Location 100</full-name>
+</record>
+<record type="resource">
+    <short-name>resource01</short-name>
+    <uid>40000000-0000-0000-0000-000000000001</uid>
+    <guid>40000000-0000-0000-0000-000000000001</guid>
+    <full-name>Resource 01</full-name>
+</record>
+<record type="resource">
+    <short-name>resource02</short-name>
+    <uid>40000000-0000-0000-0000-000000000002</uid>
+    <guid>40000000-0000-0000-0000-000000000002</guid>
+    <full-name>Resource 02</full-name>
+</record>
+<record type="resource">
+    <short-name>resource03</short-name>
+    <uid>40000000-0000-0000-0000-000000000003</uid>
+    <guid>40000000-0000-0000-0000-000000000003</guid>
+    <full-name>Resource 03</full-name>
+</record>
+<record type="resource">
+    <short-name>resource04</short-name>
+    <uid>40000000-0000-0000-0000-000000000004</uid>
+    <guid>40000000-0000-0000-0000-000000000004</guid>
+    <full-name>Resource 04</full-name>
+</record>
+<record type="resource">
+    <short-name>resource05</short-name>
+    <uid>40000000-0000-0000-0000-000000000005</uid>
+    <guid>40000000-0000-0000-0000-000000000005</guid>
+    <full-name>Resource 05</full-name>
+</record>
+<record type="resource">
+    <short-name>resource06</short-name>
+    <uid>40000000-0000-0000-0000-000000000006</uid>
+    <guid>40000000-0000-0000-0000-000000000006</guid>
+    <full-name>Resource 06</full-name>
+</record>
+<record type="resource">
+    <short-name>resource07</short-name>
+    <uid>40000000-0000-0000-0000-000000000007</uid>
+    <guid>40000000-0000-0000-0000-000000000007</guid>
+    <full-name>Resource 07</full-name>
+</record>
+<record type="resource">
+    <short-name>resource08</short-name>
+    <uid>40000000-0000-0000-0000-000000000008</uid>
+    <guid>40000000-0000-0000-0000-000000000008</guid>
+    <full-name>Resource 08</full-name>
+</record>
+<record type="resource">
+    <short-name>resource09</short-name>
+    <uid>40000000-0000-0000-0000-000000000009</uid>
+    <guid>40000000-0000-0000-0000-000000000009</guid>
+    <full-name>Resource 09</full-name>
+</record>
+<record type="resource">
+    <short-name>resource10</short-name>
+    <uid>40000000-0000-0000-0000-000000000010</uid>
+    <guid>40000000-0000-0000-0000-000000000010</guid>
+    <full-name>Resource 10</full-name>
+</record>
+<record type="resource">
+    <short-name>resource11</short-name>
+    <uid>40000000-0000-0000-0000-000000000011</uid>
+    <guid>40000000-0000-0000-0000-000000000011</guid>
+    <full-name>Resource 11</full-name>
+</record>
+<record type="resource">
+    <short-name>resource12</short-name>
+    <uid>40000000-0000-0000-0000-000000000012</uid>
+    <guid>40000000-0000-0000-0000-000000000012</guid>
+    <full-name>Resource 12</full-name>
+</record>
+<record type="resource">
+    <short-name>resource13</short-name>
+    <uid>40000000-0000-0000-0000-000000000013</uid>
+    <guid>40000000-0000-0000-0000-000000000013</guid>
+    <full-name>Resource 13</full-name>
+</record>
+<record type="resource">
+    <short-name>resource14</short-name>
+    <uid>40000000-0000-0000-0000-000000000014</uid>
+    <guid>40000000-0000-0000-0000-000000000014</guid>
+    <full-name>Resource 14</full-name>
+</record>
+<record type="resource">
+    <short-name>resource15</short-name>
+    <uid>40000000-0000-0000-0000-000000000015</uid>
+    <guid>40000000-0000-0000-0000-000000000015</guid>
+    <full-name>Resource 15</full-name>
+</record>
+<record type="resource">
+    <short-name>resource16</short-name>
+    <uid>40000000-0000-0000-0000-000000000016</uid>
+    <guid>40000000-0000-0000-0000-000000000016</guid>
+    <full-name>Resource 16</full-name>
+</record>
+<record type="resource">
+    <short-name>resource17</short-name>
+    <uid>40000000-0000-0000-0000-000000000017</uid>
+    <guid>40000000-0000-0000-0000-000000000017</guid>
+    <full-name>Resource 17</full-name>
+</record>
+<record type="resource">
+    <short-name>resource18</short-name>
+    <uid>40000000-0000-0000-0000-000000000018</uid>
+    <guid>40000000-0000-0000-0000-000000000018</guid>
+    <full-name>Resource 18</full-name>
+</record>
+<record type="resource">
+    <short-name>resource19</short-name>
+    <uid>40000000-0000-0000-0000-000000000019</uid>
+    <guid>40000000-0000-0000-0000-000000000019</guid>
+    <full-name>Resource 19</full-name>
+</record>
+<record type="resource">
+    <short-name>resource20</short-name>
+    <uid>40000000-0000-0000-0000-000000000020</uid>
+    <guid>40000000-0000-0000-0000-000000000020</guid>
+    <full-name>Resource 20</full-name>
+</record>
+<record type="resource">
+    <short-name>resource21</short-name>
+    <uid>40000000-0000-0000-0000-000000000021</uid>
+    <guid>40000000-0000-0000-0000-000000000021</guid>
+    <full-name>Resource 21</full-name>
+</record>
+<record type="resource">
+    <short-name>resource22</short-name>
+    <uid>40000000-0000-0000-0000-000000000022</uid>
+    <guid>40000000-0000-0000-0000-000000000022</guid>
+    <full-name>Resource 22</full-name>
+</record>
+<record type="resource">
+    <short-name>resource23</short-name>
+    <uid>40000000-0000-0000-0000-000000000023</uid>
+    <guid>40000000-0000-0000-0000-000000000023</guid>
+    <full-name>Resource 23</full-name>
+</record>
+<record type="resource">
+    <short-name>resource24</short-name>
+    <uid>40000000-0000-0000-0000-000000000024</uid>
+    <guid>40000000-0000-0000-0000-000000000024</guid>
+    <full-name>Resource 24</full-name>
+</record>
+<record type="resource">
+    <short-name>resource25</short-name>
+    <uid>40000000-0000-0000-0000-000000000025</uid>
+    <guid>40000000-0000-0000-0000-000000000025</guid>
+    <full-name>Resource 25</full-name>
+</record>
+<record type="resource">
+    <short-name>resource26</short-name>
+    <uid>40000000-0000-0000-0000-000000000026</uid>
+    <guid>40000000-0000-0000-0000-000000000026</guid>
+    <full-name>Resource 26</full-name>
+</record>
+<record type="resource">
+    <short-name>resource27</short-name>
+    <uid>40000000-0000-0000-0000-000000000027</uid>
+    <guid>40000000-0000-0000-0000-000000000027</guid>
+    <full-name>Resource 27</full-name>
+</record>
+<record type="resource">
+    <short-name>resource28</short-name>
+    <uid>40000000-0000-0000-0000-000000000028</uid>
+    <guid>40000000-0000-0000-0000-000000000028</guid>
+    <full-name>Resource 28</full-name>
+</record>
+<record type="resource">
+    <short-name>resource29</short-name>
+    <uid>40000000-0000-0000-0000-000000000029</uid>
+    <guid>40000000-0000-0000-0000-000000000029</guid>
+    <full-name>Resource 29</full-name>
+</record>
+<record type="resource">
+    <short-name>resource30</short-name>
+    <uid>40000000-0000-0000-0000-000000000030</uid>
+    <guid>40000000-0000-0000-0000-000000000030</guid>
+    <full-name>Resource 30</full-name>
+</record>
+<record type="resource">
+    <short-name>resource31</short-name>
+    <uid>40000000-0000-0000-0000-000000000031</uid>
+    <guid>40000000-0000-0000-0000-000000000031</guid>
+    <full-name>Resource 31</full-name>
+</record>
+<record type="resource">
+    <short-name>resource32</short-name>
+    <uid>40000000-0000-0000-0000-000000000032</uid>
+    <guid>40000000-0000-0000-0000-000000000032</guid>
+    <full-name>Resource 32</full-name>
+</record>
+<record type="resource">
+    <short-name>resource33</short-name>
+    <uid>40000000-0000-0000-0000-000000000033</uid>
+    <guid>40000000-0000-0000-0000-000000000033</guid>
+    <full-name>Resource 33</full-name>
+</record>
+<record type="resource">
+    <short-name>resource34</short-name>
+    <uid>40000000-0000-0000-0000-000000000034</uid>
+    <guid>40000000-0000-0000-0000-000000000034</guid>
+    <full-name>Resource 34</full-name>
+</record>
+<record type="resource">
+    <short-name>resource35</short-name>
+    <uid>40000000-0000-0000-0000-000000000035</uid>
+    <guid>40000000-0000-0000-0000-000000000035</guid>
+    <full-name>Resource 35</full-name>
+</record>
+<record type="resource">
+    <short-name>resource36</short-name>
+    <uid>40000000-0000-0000-0000-000000000036</uid>
+    <guid>40000000-0000-0000-0000-000000000036</guid>
+    <full-name>Resource 36</full-name>
+</record>
+<record type="resource">
+    <short-name>resource37</short-name>
+    <uid>40000000-0000-0000-0000-000000000037</uid>
+    <guid>40000000-0000-0000-0000-000000000037</guid>
+    <full-name>Resource 37</full-name>
+</record>
+<record type="resource">
+    <short-name>resource38</short-name>
+    <uid>40000000-0000-0000-0000-000000000038</uid>
+    <guid>40000000-0000-0000-0000-000000000038</guid>
+    <full-name>Resource 38</full-name>
+</record>
+<record type="resource">
+    <short-name>resource39</short-name>
+    <uid>40000000-0000-0000-0000-000000000039</uid>
+    <guid>40000000-0000-0000-0000-000000000039</guid>
+    <full-name>Resource 39</full-name>
+</record>
+<record type="resource">
+    <short-name>resource40</short-name>
+    <uid>40000000-0000-0000-0000-000000000040</uid>
+    <guid>40000000-0000-0000-0000-000000000040</guid>
+    <full-name>Resource 40</full-name>
+</record>
+<record type="resource">
+    <short-name>resource41</short-name>
+    <uid>40000000-0000-0000-0000-000000000041</uid>
+    <guid>40000000-0000-0000-0000-000000000041</guid>
+    <full-name>Resource 41</full-name>
+</record>
+<record type="resource">
+    <short-name>resource42</short-name>
+    <uid>40000000-0000-0000-0000-000000000042</uid>
+    <guid>40000000-0000-0000-0000-000000000042</guid>
+    <full-name>Resource 42</full-name>
+</record>
+<record type="resource">
+    <short-name>resource43</short-name>
+    <uid>40000000-0000-0000-0000-000000000043</uid>
+    <guid>40000000-0000-0000-0000-000000000043</guid>
+    <full-name>Resource 43</full-name>
+</record>
+<record type="resource">
+    <short-name>resource44</short-name>
+    <uid>40000000-0000-0000-0000-000000000044</uid>
+    <guid>40000000-0000-0000-0000-000000000044</guid>
+    <full-name>Resource 44</full-name>
+</record>
+<record type="resource">
+    <short-name>resource45</short-name>
+    <uid>40000000-0000-0000-0000-000000000045</uid>
+    <guid>40000000-0000-0000-0000-000000000045</guid>
+    <full-name>Resource 45</full-name>
+</record>
+<record type="resource">
+    <short-name>resource46</short-name>
+    <uid>40000000-0000-0000-0000-000000000046</uid>
+    <guid>40000000-0000-0000-0000-000000000046</guid>
+    <full-name>Resource 46</full-name>
+</record>
+<record type="resource">
+    <short-name>resource47</short-name>
+    <uid>40000000-0000-0000-0000-000000000047</uid>
+    <guid>40000000-0000-0000-0000-000000000047</guid>
+    <full-name>Resource 47</full-name>
+</record>
+<record type="resource">
+    <short-name>resource48</short-name>
+    <uid>40000000-0000-0000-0000-000000000048</uid>
+    <guid>40000000-0000-0000-0000-000000000048</guid>
+    <full-name>Resource 48</full-name>
+</record>
+<record type="resource">
+    <short-name>resource49</short-name>
+    <uid>40000000-0000-0000-0000-000000000049</uid>
+    <guid>40000000-0000-0000-0000-000000000049</guid>
+    <full-name>Resource 49</full-name>
+</record>
+<record type="resource">
+    <short-name>resource50</short-name>
+    <uid>40000000-0000-0000-0000-000000000050</uid>
+    <guid>40000000-0000-0000-0000-000000000050</guid>
+    <full-name>Resource 50</full-name>
+</record>
+<record type="resource">
+    <short-name>resource51</short-name>
+    <uid>40000000-0000-0000-0000-000000000051</uid>
+    <guid>40000000-0000-0000-0000-000000000051</guid>
+    <full-name>Resource 51</full-name>
+</record>
+<record type="resource">
+    <short-name>resource52</short-name>
+    <uid>40000000-0000-0000-0000-000000000052</uid>
+    <guid>40000000-0000-0000-0000-000000000052</guid>
+    <full-name>Resource 52</full-name>
+</record>
+<record type="resource">
+    <short-name>resource53</short-name>
+    <uid>40000000-0000-0000-0000-000000000053</uid>
+    <guid>40000000-0000-0000-0000-000000000053</guid>
+    <full-name>Resource 53</full-name>
+</record>
+<record type="resource">
+    <short-name>resource54</short-name>
+    <uid>40000000-0000-0000-0000-000000000054</uid>
+    <guid>40000000-0000-0000-0000-000000000054</guid>
+    <full-name>Resource 54</full-name>
+</record>
+<record type="resource">
+    <short-name>resource55</short-name>
+    <uid>40000000-0000-0000-0000-000000000055</uid>
+    <guid>40000000-0000-0000-0000-000000000055</guid>
+    <full-name>Resource 55</full-name>
+</record>
+<record type="resource">
+    <short-name>resource56</short-name>
+    <uid>40000000-0000-0000-0000-000000000056</uid>
+    <guid>40000000-0000-0000-0000-000000000056</guid>
+    <full-name>Resource 56</full-name>
+</record>
+<record type="resource">
+    <short-name>resource57</short-name>
+    <uid>40000000-0000-0000-0000-000000000057</uid>
+    <guid>40000000-0000-0000-0000-000000000057</guid>
+    <full-name>Resource 57</full-name>
+</record>
+<record type="resource">
+    <short-name>resource58</short-name>
+    <uid>40000000-0000-0000-0000-000000000058</uid>
+    <guid>40000000-0000-0000-0000-000000000058</guid>
+    <full-name>Resource 58</full-name>
+</record>
+<record type="resource">
+    <short-name>resource59</short-name>
+    <uid>40000000-0000-0000-0000-000000000059</uid>
+    <guid>40000000-0000-0000-0000-000000000059</guid>
+    <full-name>Resource 59</full-name>
+</record>
+<record type="resource">
+    <short-name>resource60</short-name>
+    <uid>40000000-0000-0000-0000-000000000060</uid>
+    <guid>40000000-0000-0000-0000-000000000060</guid>
+    <full-name>Resource 60</full-name>
+</record>
+<record type="resource">
+    <short-name>resource61</short-name>
+    <uid>40000000-0000-0000-0000-000000000061</uid>
+    <guid>40000000-0000-0000-0000-000000000061</guid>
+    <full-name>Resource 61</full-name>
+</record>
+<record type="resource">
+    <short-name>resource62</short-name>
+    <uid>40000000-0000-0000-0000-000000000062</uid>
+    <guid>40000000-0000-0000-0000-000000000062</guid>
+    <full-name>Resource 62</full-name>
+</record>
+<record type="resource">
+    <short-name>resource63</short-name>
+    <uid>40000000-0000-0000-0000-000000000063</uid>
+    <guid>40000000-0000-0000-0000-000000000063</guid>
+    <full-name>Resource 63</full-name>
+</record>
+<record type="resource">
+    <short-name>resource64</short-name>
+    <uid>40000000-0000-0000-0000-000000000064</uid>
+    <guid>40000000-0000-0000-0000-000000000064</guid>
+    <full-name>Resource 64</full-name>
+</record>
+<record type="resource">
+    <short-name>resource65</short-name>
+    <uid>40000000-0000-0000-0000-000000000065</uid>
+    <guid>40000000-0000-0000-0000-000000000065</guid>
+    <full-name>Resource 65</full-name>
+</record>
+<record type="resource">
+    <short-name>resource66</short-name>
+    <uid>40000000-0000-0000-0000-000000000066</uid>
+    <guid>40000000-0000-0000-0000-000000000066</guid>
+    <full-name>Resource 66</full-name>
+</record>
+<record type="resource">
+    <short-name>resource67</short-name>
+    <uid>40000000-0000-0000-0000-000000000067</uid>
+    <guid>40000000-0000-0000-0000-000000000067</guid>
+    <full-name>Resource 67</full-name>
+</record>
+<record type="resource">
+    <short-name>resource68</short-name>
+    <uid>40000000-0000-0000-0000-000000000068</uid>
+    <guid>40000000-0000-0000-0000-000000000068</guid>
+    <full-name>Resource 68</full-name>
+</record>
+<record type="resource">
+    <short-name>resource69</short-name>
+    <uid>40000000-0000-0000-0000-000000000069</uid>
+    <guid>40000000-0000-0000-0000-000000000069</guid>
+    <full-name>Resource 69</full-name>
+</record>
+<record type="resource">
+    <short-name>resource70</short-name>
+    <uid>40000000-0000-0000-0000-000000000070</uid>
+    <guid>40000000-0000-0000-0000-000000000070</guid>
+    <full-name>Resource 70</full-name>
+</record>
+<record type="resource">
+    <short-name>resource71</short-name>
+    <uid>40000000-0000-0000-0000-000000000071</uid>
+    <guid>40000000-0000-0000-0000-000000000071</guid>
+    <full-name>Resource 71</full-name>
+</record>
+<record type="resource">
+    <short-name>resource72</short-name>
+    <uid>40000000-0000-0000-0000-000000000072</uid>
+    <guid>40000000-0000-0000-0000-000000000072</guid>
+    <full-name>Resource 72</full-name>
+</record>
+<record type="resource">
+    <short-name>resource73</short-name>
+    <uid>40000000-0000-0000-0000-000000000073</uid>
+    <guid>40000000-0000-0000-0000-000000000073</guid>
+    <full-name>Resource 73</full-name>
+</record>
+<record type="resource">
+    <short-name>resource74</short-name>
+    <uid>40000000-0000-0000-0000-000000000074</uid>
+    <guid>40000000-0000-0000-0000-000000000074</guid>
+    <full-name>Resource 74</full-name>
+</record>
+<record type="resource">
+    <short-name>resource75</short-name>
+    <uid>40000000-0000-0000-0000-000000000075</uid>
+    <guid>40000000-0000-0000-0000-000000000075</guid>
+    <full-name>Resource 75</full-name>
+</record>
+<record type="resource">
+    <short-name>resource76</short-name>
+    <uid>40000000-0000-0000-0000-000000000076</uid>
+    <guid>40000000-0000-0000-0000-000000000076</guid>
+    <full-name>Resource 76</full-name>
+</record>
+<record type="resource">
+    <short-name>resource77</short-name>
+    <uid>40000000-0000-0000-0000-000000000077</uid>
+    <guid>40000000-0000-0000-0000-000000000077</guid>
+    <full-name>Resource 77</full-name>
+</record>
+<record type="resource">
+    <short-name>resource78</short-name>
+    <uid>40000000-0000-0000-0000-000000000078</uid>
+    <guid>40000000-0000-0000-0000-000000000078</guid>
+    <full-name>Resource 78</full-name>
+</record>
+<record type="resource">
+    <short-name>resource79</short-name>
+    <uid>40000000-0000-0000-0000-000000000079</uid>
+    <guid>40000000-0000-0000-0000-000000000079</guid>
+    <full-name>Resource 79</full-name>
+</record>
+<record type="resource">
+    <short-name>resource80</short-name>
+    <uid>40000000-0000-0000-0000-000000000080</uid>
+    <guid>40000000-0000-0000-0000-000000000080</guid>
+    <full-name>Resource 80</full-name>
+</record>
+<record type="resource">
+    <short-name>resource81</short-name>
+    <uid>40000000-0000-0000-0000-000000000081</uid>
+    <guid>40000000-0000-0000-0000-000000000081</guid>
+    <full-name>Resource 81</full-name>
+</record>
+<record type="resource">
+    <short-name>resource82</short-name>
+    <uid>40000000-0000-0000-0000-000000000082</uid>
+    <guid>40000000-0000-0000-0000-000000000082</guid>
+    <full-name>Resource 82</full-name>
+</record>
+<record type="resource">
+    <short-name>resource83</short-name>
+    <uid>40000000-0000-0000-0000-000000000083</uid>
+    <guid>40000000-0000-0000-0000-000000000083</guid>
+    <full-name>Resource 83</full-name>
+</record>
+<record type="resource">
+    <short-name>resource84</short-name>
+    <uid>40000000-0000-0000-0000-000000000084</uid>
+    <guid>40000000-0000-0000-0000-000000000084</guid>
+    <full-name>Resource 84</full-name>
+</record>
+<record type="resource">
+    <short-name>resource85</short-name>
+    <uid>40000000-0000-0000-0000-000000000085</uid>
+    <guid>40000000-0000-0000-0000-000000000085</guid>
+    <full-name>Resource 85</full-name>
+</record>
+<record type="resource">
+    <short-name>resource86</short-name>
+    <uid>40000000-0000-0000-0000-000000000086</uid>
+    <guid>40000000-0000-0000-0000-000000000086</guid>
+    <full-name>Resource 86</full-name>
+</record>
+<record type="resource">
+    <short-name>resource87</short-name>
+    <uid>40000000-0000-0000-0000-000000000087</uid>
+    <guid>40000000-0000-0000-0000-000000000087</guid>
+    <full-name>Resource 87</full-name>
+</record>
+<record type="resource">
+    <short-name>resource88</short-name>
+    <uid>40000000-0000-0000-0000-000000000088</uid>
+    <guid>40000000-0000-0000-0000-000000000088</guid>
+    <full-name>Resource 88</full-name>
+</record>
+<record type="resource">
+    <short-name>resource89</short-name>
+    <uid>40000000-0000-0000-0000-000000000089</uid>
+    <guid>40000000-0000-0000-0000-000000000089</guid>
+    <full-name>Resource 89</full-name>
+</record>
+<record type="resource">
+    <short-name>resource90</short-name>
+    <uid>40000000-0000-0000-0000-000000000090</uid>
+    <guid>40000000-0000-0000-0000-000000000090</guid>
+    <full-name>Resource 90</full-name>
+</record>
+<record type="resource">
+    <short-name>resource91</short-name>
+    <uid>40000000-0000-0000-0000-000000000091</uid>
+    <guid>40000000-0000-0000-0000-000000000091</guid>
+    <full-name>Resource 91</full-name>
+</record>
+<record type="resource">
+    <short-name>resource92</short-name>
+    <uid>40000000-0000-0000-0000-000000000092</uid>
+    <guid>40000000-0000-0000-0000-000000000092</guid>
+    <full-name>Resource 92</full-name>
+</record>
+<record type="resource">
+    <short-name>resource93</short-name>
+    <uid>40000000-0000-0000-0000-000000000093</uid>
+    <guid>40000000-0000-0000-0000-000000000093</guid>
+    <full-name>Resource 93</full-name>
+</record>
+<record type="resource">
+    <short-name>resource94</short-name>
+    <uid>40000000-0000-0000-0000-000000000094</uid>
+    <guid>40000000-0000-0000-0000-000000000094</guid>
+    <full-name>Resource 94</full-name>
+</record>
+<record type="resource">
+    <short-name>resource95</short-name>
+    <uid>40000000-0000-0000-0000-000000000095</uid>
+    <guid>40000000-0000-0000-0000-000000000095</guid>
+    <full-name>Resource 95</full-name>
+</record>
+<record type="resource">
+    <short-name>resource96</short-name>
+    <uid>40000000-0000-0000-0000-000000000096</uid>
+    <guid>40000000-0000-0000-0000-000000000096</guid>
+    <full-name>Resource 96</full-name>
+</record>
+<record type="resource">
+    <short-name>resource97</short-name>
+    <uid>40000000-0000-0000-0000-000000000097</uid>
+    <guid>40000000-0000-0000-0000-000000000097</guid>
+    <full-name>Resource 97</full-name>
+</record>
+<record type="resource">
+    <short-name>resource98</short-name>
+    <uid>40000000-0000-0000-0000-000000000098</uid>
+    <guid>40000000-0000-0000-0000-000000000098</guid>
+    <full-name>Resource 98</full-name>
+</record>
+<record type="resource">
+    <short-name>resource99</short-name>
+    <uid>40000000-0000-0000-0000-000000000099</uid>
+    <guid>40000000-0000-0000-0000-000000000099</guid>
+    <full-name>Resource 99</full-name>
+</record>
+<record type="resource">
+    <short-name>resource100</short-name>
+    <uid>40000000-0000-0000-0000-000000000100</uid>
+    <guid>40000000-0000-0000-0000-000000000100</guid>
+    <full-name>Resource 100</full-name>
+</record>
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/conf/caldavd-test.plist	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/conf/caldavd-test.plist	2014-04-04 17:20:27 UTC (rev 13158)
@@ -464,7 +464,7 @@
     <!-- Principals with "DAV:all" access (relative URLs) -->
     <key>AdminPrincipals</key>
     <array>
-      <string>/principals/__uids__/admin/</string>
+      <string>/principals/__uids__/0C8BDE62-E600-4696-83D3-8B5ECABDFD2E/</string>
     </array>
 
     <!-- Principals with "DAV:read" access (relative URLs) -->
@@ -577,7 +577,7 @@
 
     <!-- Log levels -->
     <key>DefaultLogLevel</key>
-    <string>info</string> <!-- debug, info, warn, error -->
+    <string>debug</string> <!-- debug, info, warn, error -->
 
     <!-- Log level overrides for specific functionality -->
     <key>LogLevels</key>
@@ -1017,6 +1017,8 @@
       <string>en</string>
     </dict>
 
-
+    <!-- Directory Address Book -->
+    <key>EnableSearchAddressBook</key>
+    <true/>
   </dict>
 </plist>

Modified: CalendarServer/branches/users/sagen/move2who-5/contrib/performance/loadtest/test_sim.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/contrib/performance/loadtest/test_sim.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/contrib/performance/loadtest/test_sim.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -24,32 +24,34 @@
 from twisted.internet.defer import Deferred, succeed
 from twisted.trial.unittest import TestCase
 
-from twistedcaldav.directory.directory import DirectoryRecord
-
 from contrib.performance.stats import NormalDistribution
 from contrib.performance.loadtest.ical import OS_X_10_6
 from contrib.performance.loadtest.profiles import Eventer, Inviter, Accepter
 from contrib.performance.loadtest.population import (
     SmoothRampUp, ClientType, PopulationParameters, Populator, CalendarClientSimulator,
-    ProfileType, SimpleStatistics)
+    ProfileType, SimpleStatistics
+)
 from contrib.performance.loadtest.sim import (
-    Arrival, SimOptions, LoadSimulator, LagTrackingReactor)
+    Arrival, SimOptions, LoadSimulator, LagTrackingReactor,
+    _DirectoryRecord
+)
 
+
 VALID_CONFIG = {
     'server': 'tcp:127.0.0.1:8008',
     'webadmin': {
         'enabled': True,
         'HTTPPort': 8080,
-        },
+    },
     'arrival': {
         'factory': 'contrib.performance.loadtest.population.SmoothRampUp',
         'params': {
             'groups': 10,
             'groupSize': 1,
             'interval': 3,
-            },
         },
-    }
+    },
+}
 
 VALID_CONFIG_PLIST = writePlistToString(VALID_CONFIG)
 
@@ -104,8 +106,9 @@
     realmName = 'stub'
 
     def _user(self, name):
-        record = DirectoryRecord(self, 'user', name, (name,))
-        record.password = 'password-' + name
+        password = 'password-' + name
+        email = name + "@example.com"
+        record = _DirectoryRecord(name, password, name, email)
         return record
 
 
@@ -119,10 +122,10 @@
             [self._user('alice'), self._user('bob'), self._user('carol')],
             Populator(None), None, None, 'http://example.org:1234/', None, None)
         users = sorted([
-                calsim._createUser(0)[0],
-                calsim._createUser(1)[0],
-                calsim._createUser(2)[0],
-                ])
+            calsim._createUser(0)[0],
+            calsim._createUser(1)[0],
+            calsim._createUser(2)[0],
+        ])
         self.assertEqual(['alice', 'bob', 'carol'], users)
 
 
@@ -171,8 +174,9 @@
 
         params = PopulationParameters()
         params.addClient(1, ClientType(
-                BrokenClient, {'runResult': clientRunResult},
-                [ProfileType(BrokenProfile, {'runResult': profileRunResult})]))
+            BrokenClient, {'runResult': clientRunResult},
+            [ProfileType(BrokenProfile, {'runResult': profileRunResult})])
+        )
         sim = CalendarClientSimulator(
             [self._user('alice')], Populator(None), params, None, 'http://example.com:1234/', None, None)
         sim.add(1, 1)
@@ -284,8 +288,9 @@
         config["accounts"] = {
             "loader": "contrib.performance.loadtest.sim.recordsFromCSVFile",
             "params": {
-                "path": accounts.path},
-            }
+                "path": accounts.path
+            },
+        }
         configpath = FilePath(self.mktemp())
         configpath.setContent(writePlistToString(config))
         io = StringIO()
@@ -312,8 +317,9 @@
         config["accounts"] = {
             "loader": "contrib.performance.loadtest.sim.recordsFromCSVFile",
             "params": {
-                "path": ""},
-            }
+                "path": ""
+            },
+        }
         configpath = FilePath(self.mktemp())
         configpath.setContent(writePlistToString(config))
         sim = LoadSimulator.fromCommandLine(['--config', configpath.path],
@@ -406,8 +412,9 @@
         section of the configuration file specified.
         """
         config = FilePath(self.mktemp())
-        config.setContent(writePlistToString({
-                    "server": "https://127.0.0.3:8432/"}))
+        config.setContent(
+            writePlistToString({"server": "https://127.0.0.3:8432/"})
+        )
         sim = LoadSimulator.fromCommandLine(['--config', config.path])
         self.assertEquals(sim.server, "https://127.0.0.3:8432/")
 
@@ -418,16 +425,18 @@
         [arrival] section of the configuration file specified.
         """
         config = FilePath(self.mktemp())
-        config.setContent(writePlistToString({
-                    "arrival": {
-                        "factory": "contrib.performance.loadtest.population.SmoothRampUp",
-                        "params": {
-                            "groups": 10,
-                            "groupSize": 1,
-                            "interval": 3,
-                            },
-                        },
-                    }))
+        config.setContent(
+            writePlistToString({
+                "arrival": {
+                    "factory": "contrib.performance.loadtest.population.SmoothRampUp",
+                    "params": {
+                        "groups": 10,
+                        "groupSize": 1,
+                        "interval": 3,
+                    },
+                },
+            })
+        )
         sim = LoadSimulator.fromCommandLine(['--config', config.path])
         self.assertEquals(
             sim.arrival,
@@ -461,11 +470,17 @@
         section of the configuration file specified.
         """
         config = FilePath(self.mktemp())
-        config.setContent(writePlistToString({
-                    "clients": [{
+        config.setContent(
+            writePlistToString(
+                {
+                    "clients": [
+                        {
                             "software": "contrib.performance.loadtest.ical.OS_X_10_6",
-                            "params": {"foo": "bar"},
-                            "profiles": [{
+                            "params": {
+                                "foo": "bar"
+                            },
+                            "profiles": [
+                                {
                                     "params": {
                                         "interval": 25,
                                         "eventStartDistribution": {
@@ -473,19 +488,38 @@
                                             "params": {
                                                 "mu": 123,
                                                 "sigma": 456,
-                                                }}},
-                                    "class": "contrib.performance.loadtest.profiles.Eventer"}],
+                                            }
+                                        }
+                                    },
+                                    "class": "contrib.performance.loadtest.profiles.Eventer"
+                                }
+                            ],
                             "weight": 3,
-                            }]}))
+                        }
+                    ]
+                }
+            )
+        )
 
         sim = LoadSimulator.fromCommandLine(
             ['--config', config.path, '--clients', config.path]
         )
         expectedParameters = PopulationParameters()
         expectedParameters.addClient(
-            3, ClientType(OS_X_10_6, {"foo": "bar"}, [ProfileType(Eventer, {
+            3,
+            ClientType(
+                OS_X_10_6,
+                {"foo": "bar"},
+                [
+                    ProfileType(
+                        Eventer, {
                             "interval": 25,
-                            "eventStartDistribution": NormalDistribution(123, 456)})]))
+                            "eventStartDistribution": NormalDistribution(123, 456)
+                        }
+                    )
+                ]
+            )
+        )
         self.assertEquals(sim.parameters, expectedParameters)
 
 
@@ -512,9 +546,18 @@
         configuration file are added to the logging system.
         """
         config = FilePath(self.mktemp())
-        config.setContent(writePlistToString({
-            "observers": [{"type":"contrib.performance.loadtest.population.SimpleStatistics", "params":{}, }, ]
-        }))
+        config.setContent(
+            writePlistToString(
+                {
+                    "observers": [
+                        {
+                            "type": "contrib.performance.loadtest.population.SimpleStatistics",
+                            "params": {},
+                        },
+                    ]
+                }
+            )
+        )
         sim = LoadSimulator.fromCommandLine(['--config', config.path])
         self.assertEquals(len(sim.observers), 1)
         self.assertIsInstance(sim.observers[0], SimpleStatistics)

Modified: CalendarServer/branches/users/sagen/move2who-5/requirements/py_develop.txt
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/requirements/py_develop.txt	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/requirements/py_develop.txt	2014-04-04 17:20:27 UTC (rev 13158)
@@ -4,7 +4,9 @@
 
 pyflakes
 docutils>=0.11
+mockldap>=0.1.4
+q
 
 -e svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk#egg=CalDAVClientLibrary
 
--e svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk#egg=CalDAVTester
+-e svn+http://svn.calendarserver.org/repository/calendarserver/CalendarServer/branches/users/sagen/move2who-cdt#egg=CalDAVTester

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/cache.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/cache.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/cache.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -69,6 +69,7 @@
 
 """
 
+
 class DisabledCacheNotifier(object):
     def __init__(self, *args, **kwargs):
         pass
@@ -164,6 +165,7 @@
             raise URINotFoundException(uri)
 
 
+    @inlineCallbacks
     def _canonicalizeURIForRequest(self, uri, request):
         """
         Always use canonicalized forms of the URIs for caching (i.e. __uids__ paths).
@@ -174,21 +176,24 @@
         uribits = uri.split("/")
         if len(uribits) > 1 and uribits[1] in ("principals", "calendars", "addressbooks"):
             if uribits[2] == "__uids__":
-                return succeed(uri)
+                returnValue(uri)
             else:
                 recordType = uribits[2]
                 recordName = uribits[3]
                 directory = request.site.resource.getDirectory()
-                record = directory.recordWithShortName(recordType, recordName)
+                record = yield directory.recordWithShortName(
+                    directory.oldNameToRecordType(recordType),
+                    recordName
+                )
                 if record is not None:
                     uribits[2] = "__uids__"
-                    uribits[3] = record.uid
-                    return succeed("/".join(uribits))
+                    uribits[3] = record.uid.encode("utf-8")
+                    returnValue("/".join(uribits))
 
         # Fall back to the locateResource approach
         try:
-            return request.locateResource(uri).addCallback(
-                lambda resrc: resrc.url()).addErrback(self._uriNotFound, uri)
+            resrc = yield request.locateResource(uri)
+            returnValue(resrc.url())
         except AssertionError:
             raise URINotFoundException(uri)
 
@@ -252,7 +257,8 @@
         """
         Get the current token for a particular URI.
         """
-
+        if isinstance(uri, unicode):
+            uri = uri.encode("utf-8")
         if cachePoolHandle:
             result = (yield defaultCachePool(cachePoolHandle).get('cacheToken:%s' % (uri,)))
         else:
@@ -436,8 +442,9 @@
                     cTokens,
                 )
             )
-            yield self.getCachePool().set(key, cacheEntry,
-                expireTime=config.ResponseCacheTimeout * 60)
+            yield self.getCachePool().set(
+                key, cacheEntry, expireTime=config.ResponseCacheTimeout * 60
+            )
 
         except URINotFoundException, e:
             self.log.debug("Could not locate URI: {e!r}", e=e)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/customxml.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/customxml.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1456,6 +1456,8 @@
 
 ResourceType.calendarproxyread = ResourceType(Principal(), Collection(), CalendarProxyRead())
 ResourceType.calendarproxywrite = ResourceType(Principal(), Collection(), CalendarProxyWrite())
+ResourceType.calendarproxyreadfor = ResourceType(Principal(), Collection(), CalendarProxyReadFor())
+ResourceType.calendarproxywritefor = ResourceType(Principal(), Collection(), CalendarProxyWriteFor())
 
 ResourceType.timezones = ResourceType(Timezones())
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/addressbook.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/addressbook.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/addressbook.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -34,7 +34,6 @@
 from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twistedcaldav.config import config
-from twistedcaldav.directory.idirectory import IDirectoryService
 
 from twistedcaldav.directory.common import CommonUIDProvisioningResource,\
     uidsResourceName, CommonHomeTypeProvisioningResource
@@ -58,14 +57,14 @@
 
 
 
-class DirectoryAddressBookProvisioningResource (
+class DirectoryAddressBookProvisioningResource(
     ReadOnlyResourceMixIn,
     CalDAVComplianceMixIn,
     DAVResourceWithChildrenMixin,
     DAVResource,
 ):
     def defaultAccessControlList(self):
-        return config.ProvisioningResourceACL
+        return succeed(config.ProvisioningResourceACL)
 
 
     def etag(self):
@@ -77,9 +76,9 @@
 
 
 
-class DirectoryAddressBookHomeProvisioningResource (
-        DirectoryAddressBookProvisioningResource
-    ):
+class DirectoryAddressBookHomeProvisioningResource(
+    DirectoryAddressBookProvisioningResource
+):
     """
     Resource which provisions address book home collections as needed.
     """
@@ -93,19 +92,39 @@
 
         super(DirectoryAddressBookHomeProvisioningResource, self).__init__()
 
-        self.directory = IDirectoryService(directory)
+        # MOVE2WHO
+        self.directory = directory  # IDirectoryService(directory)
         self._url = url
         self._newStore = store
 
         # FIXME: Smells like a hack
         directory.addressBookHomesCollection = self
 
+
         #
         # Create children
         #
-        for recordType in self.directory.recordTypes():
-            self.putChild(recordType, DirectoryAddressBookHomeTypeProvisioningResource(self, recordType))
+        # ...just users, locations, and resources though.  If we iterate all of
+        # the directory's recordTypes, we also get the proxy sub principal types
+        # and other things which don't have addressbooks.
 
+        self.supportedChildTypes = (
+            self.directory.recordType.user,
+            self.directory.recordType.location,
+            self.directory.recordType.resource,
+        )
+
+        for recordType, recordTypeName in [
+            (r, self.directory.recordTypeToOldName(r)) for r in
+            self.supportedChildTypes
+        ]:
+            self.putChild(
+                recordTypeName,
+                DirectoryAddressBookHomeTypeProvisioningResource(
+                    self, recordTypeName, recordType
+                )
+            )
+
         self.putChild(uidsResourceName, DirectoryAddressBookHomeUIDProvisioningResource(self))
 
 
@@ -114,7 +133,10 @@
 
 
     def listChildren(self):
-        return self.directory.recordTypes()
+        return [
+            self.directory.recordTypeToOldName(r) for r in
+            self.supportedChildTypes
+        ]
 
 
     def principalCollections(self):
@@ -129,12 +151,13 @@
         return self.directory.principalCollection.principalForRecord(record)
 
 
+    @inlineCallbacks
     def homeForDirectoryRecord(self, record, request):
-        uidResource = self.getChild(uidsResourceName)
+        uidResource = yield self.getChild(uidsResourceName)
         if uidResource is None:
-            return None
+            returnValue(None)
         else:
-            return uidResource.homeResourceForRecord(record, request)
+            returnValue((yield uidResource.homeResourceForRecord(record, request)))
 
 
     ##
@@ -151,42 +174,46 @@
 
 
 class DirectoryAddressBookHomeTypeProvisioningResource (
-        CommonHomeTypeProvisioningResource,
-        DirectoryAddressBookProvisioningResource
-    ):
+    CommonHomeTypeProvisioningResource,
+    DirectoryAddressBookProvisioningResource
+):
     """
     Resource which provisions address book home collections of a specific
     record type as needed.
     """
-    def __init__(self, parent, recordType):
+    def __init__(self, parent, name, recordType):
         """
         @param parent: the parent of this resource
         @param recordType: the directory record type to provision.
         """
         assert parent is not None
+        assert name is not None
         assert recordType is not None
 
         super(DirectoryAddressBookHomeTypeProvisioningResource, self).__init__()
 
         self.directory = parent.directory
+        self.name = name
         self.recordType = recordType
         self._parent = parent
 
 
     def url(self):
-        return joinURL(self._parent.url(), self.recordType)
+        return joinURL(self._parent.url(), self.name)
 
 
+    @inlineCallbacks
     def listChildren(self):
         if config.EnablePrincipalListings:
+            children = []
+            for record in (
+                yield self.directory.recordsWithRecordType(self.recordType)
+            ):
+                if getattr(record, "hasContacts", False):
+                    for shortName in record.shortNames:
+                        children.append(shortName)
 
-            def _recordShortnameExpand():
-                for record in self.directory.listRecords(self.recordType):
-                    if record.enabledForAddressBooks:
-                        for shortName in record.shortNames:
-                            yield shortName
-
-            return _recordShortnameExpand()
+            returnValue(children)
         else:
             # Not a listable collection
             raise HTTPError(responsecode.FORBIDDEN)
@@ -205,7 +232,7 @@
 
 
     def displayName(self):
-        return self.recordType
+        return self.directory.recordTypeToOldName(self.recordType)
 
     ##
     # ACL
@@ -222,13 +249,13 @@
 
 
 class DirectoryAddressBookHomeUIDProvisioningResource (
-        CommonUIDProvisioningResource,
-        DirectoryAddressBookProvisioningResource
-    ):
+    CommonUIDProvisioningResource,
+    DirectoryAddressBookProvisioningResource
+):
 
     homeResourceTypeName = 'addressbooks'
 
-    enabledAttribute = 'enabledForAddressBooks'
+    enabledAttribute = 'hasContacts'
 
 
     def homeResourceCreator(self, record, transaction):

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/aggregate.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/aggregate.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/aggregate.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,385 +0,0 @@
-##
-# Copyright (c) 2006-2014 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.
-##
-
-"""
-Directory service implementation which aggregates multiple directory
-services.
-"""
-
-__all__ = [
-    "AggregateDirectoryService",
-    "DuplicateRecordTypeError",
-]
-
-import itertools
-from twisted.cred.error import UnauthorizedLogin
-
-from twistedcaldav.directory.idirectory import IDirectoryService
-from twistedcaldav.directory.directory import DirectoryService, DirectoryError
-from twistedcaldav.directory.directory import UnknownRecordTypeError
-from twisted.internet.defer import inlineCallbacks, returnValue
-
-class AggregateDirectoryService(DirectoryService):
-    """
-    L{IDirectoryService} implementation which aggregates multiple directory
-    services.
-
-    @ivar _recordTypes: A map of record types to L{IDirectoryService}s.
-    @type _recordTypes: L{dict} mapping L{bytes} to L{IDirectoryService}
-        provider.
-    """
-    baseGUID = "06FB225F-39E7-4D34-B1D1-29925F5E619B"
-
-    def __init__(self, services, groupMembershipCache):
-        super(AggregateDirectoryService, self).__init__()
-
-        realmName = None
-        recordTypes = {}
-        self.groupMembershipCache = groupMembershipCache
-
-        for service in services:
-            service = IDirectoryService(service)
-
-            if service.realmName != realmName:
-                assert realmName is None, (
-                    "Aggregated directory services must have the same realm name: %r != %r\nServices: %r"
-                    % (service.realmName, realmName, services)
-                )
-                realmName = service.realmName
-
-            if not hasattr(service, "recordTypePrefix"):
-                service.recordTypePrefix = ""
-            prefix = service.recordTypePrefix
-
-            for recordType in (prefix + r for r in service.recordTypes()):
-                if recordType in recordTypes:
-                    raise DuplicateRecordTypeError(
-                        "%r is in multiple services: %s, %s"
-                        % (recordType, recordTypes[recordType], service)
-                    )
-                recordTypes[recordType] = service
-
-            service.aggregateService = self
-
-        self.realmName = realmName
-        self._recordTypes = recordTypes
-
-        # FIXME: This is a temporary workaround until new data store is in
-        # place.  During the purging of deprovisioned users' data, we need
-        # to be able to look up records by uid and shortName.  The purge
-        # tool sticks temporary fake records in here.
-        self._tmpRecords = {
-            "uids" : { },
-            "shortNames" : { },
-        }
-
-
-    def __repr__(self):
-        return "<%s (%s): %r>" % (self.__class__.__name__, self.realmName, self._recordTypes)
-
-
-    #
-    # Define calendarHomesCollection as a property so we can set it on contained services
-    #
-    def _getCalendarHomesCollection(self):
-        return self._calendarHomesCollection
-
-
-    def _setCalendarHomesCollection(self, value):
-        for service in self._recordTypes.values():
-            service.calendarHomesCollection = value
-        self._calendarHomesCollection = value
-
-    calendarHomesCollection = property(_getCalendarHomesCollection, _setCalendarHomesCollection)
-
-    #
-    # Define addressBookHomesCollection as a property so we can set it on contained services
-    #
-    def _getAddressBookHomesCollection(self):
-        return self._addressBookHomesCollection
-
-
-    def _setAddressBookHomesCollection(self, value):
-        for service in self._recordTypes.values():
-            service.addressBookHomesCollection = value
-        self._addressBookHomesCollection = value
-
-    addressBookHomesCollection = property(_getAddressBookHomesCollection, _setAddressBookHomesCollection)
-
-
-    def addService(self, service):
-        """
-        Add another service to this aggregate.
-
-        @param service: the service to add
-        @type service: L{IDirectoryService}
-        """
-        service = IDirectoryService(service)
-
-        if service.realmName != self.realmName:
-            assert self.realmName is None, (
-                "Aggregated directory services must have the same realm name: %r != %r\nServices: %r"
-                % (service.realmName, self.realmName, service)
-            )
-
-        if not hasattr(service, "recordTypePrefix"):
-            service.recordTypePrefix = ""
-        prefix = service.recordTypePrefix
-
-        for recordType in (prefix + r for r in service.recordTypes()):
-            if recordType in self._recordTypes:
-                raise DuplicateRecordTypeError(
-                    "%r is in multiple services: %s, %s"
-                    % (recordType, self.recordTypes[recordType], service)
-                )
-            self._recordTypes[recordType] = service
-
-        service.aggregateService = self
-
-
-    def recordTypes(self):
-        return set(self._recordTypes)
-
-
-    def listRecords(self, recordType):
-        records = self._query("listRecords", recordType)
-        if records is None:
-            return ()
-        else:
-            return records
-
-
-    def recordWithShortName(self, recordType, shortName):
-
-        # FIXME: These temporary records shouldn't be needed when we move
-        # to the new data store API.  They're currently needed when purging
-        # deprovisioned users' data.
-        record = self._tmpRecords["shortNames"].get(shortName, None)
-        if record:
-            return record
-
-        return self._query("recordWithShortName", recordType, shortName)
-
-
-    def recordWithUID(self, uid):
-
-        # FIXME: These temporary records shouldn't be needed when we move
-        # to the new data store API.  They're currently needed when purging
-        # deprovisioned users' data.
-        record = self._tmpRecords["uids"].get(uid, None)
-        if record:
-            return record
-
-        return self._queryAll("recordWithUID", uid)
-
-    recordWithGUID = recordWithUID
-
-    def recordWithAuthID(self, authID):
-        return self._queryAll("recordWithAuthID", authID)
-
-
-    def recordWithCalendarUserAddress(self, address):
-        return self._queryAll("recordWithCalendarUserAddress", address)
-
-
-    def recordWithCachedGroupsAlias(self, recordType, alias):
-        """
-        @param recordType: the type of the record to look up.
-        @param alias: the cached-groups alias of the record to look up.
-        @type alias: C{str}
-
-        @return: a deferred L{IDirectoryRecord} with the given cached-groups
-            alias, or C{None} if no such record is found.
-        """
-        service = self.serviceForRecordType(recordType)
-        return service.recordWithCachedGroupsAlias(recordType, alias)
-
-
-    @inlineCallbacks
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-
-        if recordType:
-            services = (self.serviceForRecordType(recordType),)
-        else:
-            services = set(self._recordTypes.values())
-
-        generators = []
-        for service in services:
-            generator = (yield service.recordsMatchingFields(fields,
-                operand=operand, recordType=recordType))
-            generators.append(generator)
-
-        returnValue(itertools.chain(*generators))
-
-
-    @inlineCallbacks
-    def recordsMatchingTokens(self, tokens, context=None):
-        """
-        Combine the results from the sub-services.
-
-        Each token is searched for within each record's full name and email
-        address; if each token is found within a record that record is returned
-        in the results.
-
-        If context is None, all record types are considered.  If context is
-        "location", only locations are considered.  If context is "attendee",
-        only users, groups, and resources are considered.
-
-        @param tokens: The tokens to search on
-        @type tokens: C{list} of C{str} (utf-8 bytes)
-
-        @param context: An indication of what the end user is searching for;
-            "attendee", "location", or None
-        @type context: C{str}
-
-        @return: a deferred sequence of L{IDirectoryRecord}s which match the
-            given tokens and optional context.
-        """
-
-        services = set(self._recordTypes.values())
-
-        generators = []
-        for service in services:
-            generator = (yield service.recordsMatchingTokens(tokens,
-                context=context))
-            generators.append(generator)
-
-        returnValue(itertools.chain(*generators))
-
-
-    def getGroups(self, guids):
-        """
-        Returns a set of group records for the list of guids passed in.  For
-        any group that also contains subgroups, those subgroups' records are
-        also returned, and so on.
-        """
-        recordType = self.recordType_groups
-        service = self.serviceForRecordType(recordType)
-        return service.getGroups(guids)
-
-
-    def serviceForRecordType(self, recordType):
-        try:
-            return self._recordTypes[recordType]
-        except KeyError:
-            raise UnknownRecordTypeError(recordType)
-
-
-    def _query(self, query, recordType, *args):
-        try:
-            service = self.serviceForRecordType(recordType)
-        except UnknownRecordTypeError:
-            return None
-
-        return getattr(service, query)(
-            recordType[len(service.recordTypePrefix):],
-            *[a[len(service.recordTypePrefix):] for a in args]
-        )
-
-
-    def _queryAll(self, query, *args):
-        for service in self._recordTypes.values():
-            try:
-                record = getattr(service, query)(*args)
-            except UnknownRecordTypeError:
-                record = None
-            if record is not None:
-                return record
-        else:
-            return None
-
-
-    def flushCaches(self):
-        for service in self._recordTypes.values():
-            if hasattr(service, "_initCaches"):
-                service._initCaches()
-
-    userRecordTypes = [DirectoryService.recordType_users]
-
-    def requestAvatarId(self, credentials):
-
-        if credentials.authnPrincipal:
-            return credentials.authnPrincipal.record.service.requestAvatarId(credentials)
-
-        raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
-
-
-    def getResourceInfo(self):
-        results = []
-        for service in self._recordTypes.values():
-            for result in service.getResourceInfo():
-                if result:
-                    results.append(result)
-        return results
-
-
-    def getExternalProxyAssignments(self):
-        service = self.serviceForRecordType(self.recordType_locations)
-        return service.getExternalProxyAssignments()
-
-
-    def createRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, password=None, **kwargs):
-        service = self.serviceForRecordType(recordType)
-        return service.createRecord(recordType, guid=guid,
-            shortNames=shortNames, authIDs=authIDs, fullName=fullName,
-            firstName=firstName, lastName=lastName,
-            emailAddresses=emailAddresses, uid=uid, password=password, **kwargs)
-
-
-    def updateRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, password=None, **kwargs):
-        service = self.serviceForRecordType(recordType)
-        return service.updateRecord(recordType, guid=guid,
-            shortNames=shortNames,
-            authIDs=authIDs, fullName=fullName, firstName=firstName,
-            lastName=lastName, emailAddresses=emailAddresses, uid=uid,
-            password=password, **kwargs)
-
-
-    def destroyRecord(self, recordType, guid=None):
-        service = self.serviceForRecordType(recordType)
-        return service.destroyRecord(recordType, guid=guid)
-
-
-    def setRealm(self, realmName):
-        """
-        Set a new realm name for this and nested services
-        """
-        self.realmName = realmName
-        for service in self._recordTypes.values():
-            service.setRealm(realmName)
-
-
-    def setPrincipalCollection(self, principalCollection):
-        """
-        Set the principal service that the directory relies on for doing proxy tests.
-
-        @param principalService: the principal service.
-        @type principalService: L{DirectoryProvisioningResource}
-        """
-        self.principalCollection = principalCollection
-        for service in self._recordTypes.values():
-            service.setPrincipalCollection(principalCollection)
-
-
-
-class DuplicateRecordTypeError(DirectoryError):
-    """
-    Duplicate record type.
-    """

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/appleopendirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/appleopendirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/appleopendirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,1584 +0,0 @@
-# -*- test-case-name: twistedcaldav.directory.test.test_opendirectory -*-
-##
-# Copyright (c) 2006-2014 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.
-##
-
-"""
-Apple OpenDirectory directory service implementation.
-"""
-
-__all__ = [
-    "OpenDirectoryService",
-    "OpenDirectoryInitError",
-]
-
-import sys
-import time
-from uuid import UUID
-
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-from twisted.cred.credentials import UsernamePassword
-from txweb2.auth.digest import DigestedCredentials
-from twext.python.log import Logger
-
-from twistedcaldav.directory.cachingdirectory import CachingDirectoryService, \
-    CachingDirectoryRecord
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
-from twistedcaldav.directory.util import splitIntoBatches
-from twistedcaldav.directory.principal import cuAddressConverter
-
-from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
-
-
-
-class OpenDirectoryService(CachingDirectoryService):
-    """
-    OpenDirectory implementation of L{IDirectoryService}.
-    """
-    log = Logger()
-
-    baseGUID = "891F8321-ED02-424C-BA72-89C32F215C1E"
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
-
-
-    def __init__(self, params, odModule=None):
-        """
-        @param params: a dictionary containing the following keys:
-
-            - node: an OpenDirectory node name to bind to.
-
-            - restrictEnabledRecords: C{True} if a group in the directory is to
-              be used to determine which calendar users are enabled.
-
-            - restrictToGroup: C{str} guid or name of group used to restrict
-              enabled users.
-
-            - cacheTimeout: C{int} number of minutes before cache is
-              invalidated.
-
-            - negativeCache: C{False} cache the fact that a record wasn't found
-        """
-        defaults = {
-            'node' : '/Search',
-            'restrictEnabledRecords' : False,
-            'restrictToGroup' : '',
-            'cacheTimeout' : 1, # Minutes
-            'batchSize' : 100, # for splitting up large queries
-            'negativeCaching' : False,
-            'recordTypes' : (
-                self.recordType_users,
-                self.recordType_groups,
-            ),
-            'augmentService' : None,
-            'groupMembershipCache' : None,
-        }
-        ignored = ('requireComputerRecord',)
-        params = self.getParams(params, defaults, ignored)
-
-        self._recordTypes = params['recordTypes']
-
-        super(OpenDirectoryService, self).__init__(params['cacheTimeout'],
-                                                   params['negativeCaching'])
-
-        if odModule is None:
-            odModule = opendirectory
-        self.odModule = odModule
-
-        try:
-            directory = self.odModule.odInit(params['node'])
-        except self.odModule.ODError, e:
-            self.log.error("OpenDirectory (node=%s) Initialization error: %s" % (params['node'], e))
-            raise
-
-        self.augmentService = params['augmentService']
-        self.groupMembershipCache = params['groupMembershipCache']
-        self.realmName = params['node']
-        self.directory = directory
-        self.node = params['node']
-        self.restrictEnabledRecords = params['restrictEnabledRecords']
-        self.restrictToGroup = params['restrictToGroup']
-        self.batchSize = params['batchSize']
-        try:
-            UUID(self.restrictToGroup)
-        except:
-            self.restrictToGUID = False
-        else:
-            self.restrictToGUID = True
-        self.restrictedTimestamp = 0
-
-        # Set up the /Local/Default node if it's in the search path so we can
-        # send custom queries to it
-        self.localNode = None
-        try:
-            if self.node == "/Search":
-                result = self.odModule.getNodeAttributes(self.directory, "/Search",
-                    (dsattributes.kDS1AttrSearchPath,))
-                if "/Local/Default" in result[dsattributes.kDS1AttrSearchPath]:
-                    try:
-                        self.localNode = self.odModule.odInit("/Local/Default")
-                    except self.odModule.ODError, e:
-                        self.log.error("Failed to open /Local/Default): %s" % (e,))
-        except AttributeError:
-            pass
-
-
-    @property
-    def restrictedGUIDs(self):
-        """
-        Look up (and cache) the set of guids that are members of the
-        restrictToGroup.  If restrictToGroup is not set, return None to
-        indicate there are no group restrictions.
-        """
-        if self.restrictEnabledRecords:
-            if time.time() - self.restrictedTimestamp > self.cacheTimeout:
-                attributeToMatch = dsattributes.kDS1AttrGeneratedUID if self.restrictToGUID else dsattributes.kDSNAttrRecordName
-                valueToMatch = self.restrictToGroup
-                self.log.debug("Doing restricted group membership check")
-                self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                    self.directory,
-                    attributeToMatch,
-                    valueToMatch,
-                    dsattributes.eDSExact,
-                    False,
-                    dsattributes.kDSStdRecordTypeGroups,
-                    [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups, ],
-                ))
-                results = self.odModule.queryRecordsWithAttribute_list(
-                    self.directory,
-                    attributeToMatch,
-                    valueToMatch,
-                    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 = []
-                self._cachedRestrictedGUIDs = set(self._expandGroupMembership(members, nestedGroups, returnGroups=True))
-                self.log.debug("Got %d restricted group members" % (len(self._cachedRestrictedGUIDs),))
-                self.restrictedTimestamp = time.time()
-            return self._cachedRestrictedGUIDs
-        else:
-            # No restrictions
-            return None
-
-
-    def __cmp__(self, other):
-        if not isinstance(other, DirectoryRecord):
-            return super(DirectoryRecord, self).__eq__(other)
-
-        for attr in ("directory", "node"):
-            diff = cmp(getattr(self, attr), getattr(other, attr))
-            if diff != 0:
-                return diff
-        return 0
-
-
-    def __hash__(self):
-        h = hash(self.__class__.__name__)
-        for attr in ("node",):
-            h = (h + hash(getattr(self, attr))) & sys.maxint
-        return h
-
-
-    def _expandGroupMembership(self, members, nestedGroups, processedGUIDs=None, returnGroups=False):
-
-        if processedGUIDs is None:
-            processedGUIDs = set()
-
-        if isinstance(members, str):
-            members = [members]
-
-        if isinstance(nestedGroups, str):
-            nestedGroups = [nestedGroups]
-
-        for memberGUID in members:
-            if memberGUID not in processedGUIDs:
-                processedGUIDs.add(memberGUID)
-                yield memberGUID
-
-        for groupGUID in nestedGroups:
-            if groupGUID in processedGUIDs:
-                continue
-
-            self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                dsattributes.kDS1AttrGeneratedUID,
-                groupGUID,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-            ))
-            result = self.odModule.queryRecordsWithAttribute_list(
-                self.directory,
-                dsattributes.kDS1AttrGeneratedUID,
-                groupGUID,
-                dsattributes.eDSExact,
-                False,
-                dsattributes.kDSStdRecordTypeGroups,
-                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
-            )
-
-            if not result:
-                self.log.error("Couldn't find group %s when trying to expand nested groups."
-                             % (groupGUID,))
-                continue
-
-            group = result[0][1]
-
-            processedGUIDs.add(groupGUID)
-            if returnGroups:
-                yield groupGUID
-
-            for GUID in self._expandGroupMembership(
-                group.get(dsattributes.kDSNAttrGroupMembers, []),
-                group.get(dsattributes.kDSNAttrNestedGroups, []),
-                processedGUIDs,
-                returnGroups,
-            ):
-                yield GUID
-
-
-    def recordTypes(self):
-        return self._recordTypes
-
-
-    def listRecords(self, recordType):
-        """
-        Retrieve all the records of recordType from the directory, but for
-        expediency don't index them or cache them locally, nor in memcached.
-        """
-
-        records = []
-
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrRecordName,
-            dsattributes.kDS1AttrDistinguishedName,
-        ]
-
-        if recordType == DirectoryService.recordType_users:
-            ODRecordType = self._toODRecordTypes[recordType]
-
-        elif recordType in (
-            DirectoryService.recordType_resources,
-            DirectoryService.recordType_locations,
-        ):
-            attrs.append(dsattributes.kDSNAttrResourceInfo)
-            ODRecordType = self._toODRecordTypes[recordType]
-
-        elif recordType == DirectoryService.recordType_groups:
-            attrs.append(dsattributes.kDSNAttrGroupMembers)
-            attrs.append(dsattributes.kDSNAttrNestedGroups)
-            ODRecordType = dsattributes.kDSStdRecordTypeGroups
-
-        self.log.debug("Querying OD for all %s records" % (recordType,))
-        results = self.odModule.listAllRecordsWithAttributes_list(
-            self.directory, ODRecordType, attrs)
-        self.log.debug("Retrieved %d %s records" % (len(results), recordType,))
-
-        for key, value in results:
-            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-            if not recordGUID:
-                self.log.warn("Ignoring record missing GUID: %s %s" %
-                    (key, value,))
-                continue
-
-            # Skip if group restriction is in place and guid is not
-            # a member (but don't skip any groups)
-            if (recordType != self.recordType_groups and
-                self.restrictedGUIDs is not None):
-                if str(recordGUID) not in self.restrictedGUIDs:
-                    continue
-
-            recordShortNames = self._uniqueTupleFromAttribute(
-                value.get(dsattributes.kDSNAttrRecordName))
-            recordFullName = value.get(
-                dsattributes.kDS1AttrDistinguishedName)
-
-            proxyGUIDs = ()
-            readOnlyProxyGUIDs = ()
-
-            if recordType in (
-                DirectoryService.recordType_resources,
-                DirectoryService.recordType_locations,
-            ):
-                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
-                if resourceInfo is not None:
-                    if type(resourceInfo) is not str:
-                        resourceInfo = resourceInfo[0]
-                    try:
-                        (
-                            _ignore_autoSchedule,
-                            proxy,
-                            readOnlyProxy
-                        ) = self.parseResourceInfo(
-                            resourceInfo,
-                            recordGUID,
-                            recordType,
-                            recordShortNames[0]
-                        )
-                    except ValueError:
-                        continue
-                    if proxy:
-                        proxyGUIDs = (proxy,)
-                    if readOnlyProxy:
-                        readOnlyProxyGUIDs = (readOnlyProxy,)
-
-            # Special case for groups, which have members.
-            if recordType == self.recordType_groups:
-                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
-                if memberGUIDs is None:
-                    memberGUIDs = ()
-                elif type(memberGUIDs) is str:
-                    memberGUIDs = (memberGUIDs,)
-                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
-                if nestedGUIDs:
-                    if type(nestedGUIDs) is str:
-                        nestedGUIDs = (nestedGUIDs,)
-                    memberGUIDs += tuple(nestedGUIDs)
-                else:
-                    nestedGUIDs = ()
-            else:
-                memberGUIDs = ()
-                nestedGUIDs = ()
-
-            record = OpenDirectoryRecord(
-                service=self,
-                recordType=recordType,
-                guid=recordGUID,
-                nodeName="",
-                shortNames=recordShortNames,
-                authIDs=(),
-                fullName=recordFullName,
-                firstName="",
-                lastName="",
-                emailAddresses="",
-                memberGUIDs=memberGUIDs,
-                nestedGUIDs=nestedGUIDs,
-                extProxies=proxyGUIDs,
-                extReadOnlyProxies=readOnlyProxyGUIDs,
-            )
-
-            # (Copied from below)
-            # 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.
-            if self.augmentService is not None:
-                d = self.augmentService.getAugmentRecord(record.guid,
-                    recordType)
-                d.addCallback(lambda x: record.addAugmentInformation(x))
-            records.append(record)
-
-        self.log.debug("ListRecords returning %d %s records" % (len(records),
-            recordType))
-
-        return records
-
-
-    def groupsForGUID(self, guid):
-
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-        ]
-
-        recordType = dsattributes.kDSStdRecordTypeGroups
-
-        guids = set()
-
-        self.log.debug("Looking up which groups %s is a member of" % (guid,))
-        try:
-            self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                dsattributes.kDSNAttrGroupMembers,
-                guid,
-                dsattributes.eDSExact,
-                False,
-                recordType,
-                attrs,
-            ))
-            results = self.odModule.queryRecordsWithAttribute_list(
-                self.directory,
-                dsattributes.kDSNAttrGroupMembers,
-                guid,
-                dsattributes.eDSExact,
-                False,
-                recordType,
-                attrs,
-            )
-        except self.odModule.ODError, ex:
-            self.log.error("OpenDirectory (node=%s) error: %s" % (self.realmName, str(ex)))
-            raise
-
-        for (_ignore_recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-            if recordGUID:
-                guids.add(recordGUID)
-
-        try:
-            self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                self.directory,
-                dsattributes.kDSNAttrNestedGroups,
-                guid,
-                dsattributes.eDSExact,
-                False,
-                recordType,
-                attrs,
-            ))
-            results = self.odModule.queryRecordsWithAttribute_list(
-                self.directory,
-                dsattributes.kDSNAttrNestedGroups,
-                guid,
-                dsattributes.eDSExact,
-                False,
-                recordType,
-                attrs,
-            )
-        except self.odModule.ODError, ex:
-            self.log.error("OpenDirectory (node=%s) error: %s" % (self.realmName, str(ex)))
-            raise
-
-        for (_ignore_recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-            if recordGUID:
-                guids.add(recordGUID)
-
-        self.log.debug("%s is a member of %d groups" % (guid, len(guids)))
-
-        return guids
-
-    _ODFields = {
-        'fullName' : {
-            'odField' : dsattributes.kDS1AttrDistinguishedName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypeResources,
-                dsattributes.kDSStdRecordTypePlaces,
-            ]),
-        },
-        'firstName' : {
-            'odField' : dsattributes.kDS1AttrFirstName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-            ]),
-        },
-        'lastName' : {
-            'odField' : dsattributes.kDS1AttrLastName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-            ]),
-        },
-        'emailAddresses' : {
-            'odField' : dsattributes.kDSNAttrEMailAddress,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-            ]),
-        },
-        'recordName' : {
-            'odField' : dsattributes.kDSNAttrRecordName,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypeResources,
-                dsattributes.kDSStdRecordTypePlaces,
-            ]),
-        },
-        'guid' : {
-            'odField' : dsattributes.kDS1AttrGeneratedUID,
-            'appliesTo' : set([
-                dsattributes.kDSStdRecordTypeUsers,
-                dsattributes.kDSStdRecordTypeGroups,
-                dsattributes.kDSStdRecordTypeResources,
-                dsattributes.kDSStdRecordTypePlaces,
-            ]),
-        },
-    }
-
-    _toODRecordTypes = {
-        DirectoryService.recordType_users :
-            dsattributes.kDSStdRecordTypeUsers,
-        DirectoryService.recordType_groups :
-            dsattributes.kDSStdRecordTypeGroups,
-        DirectoryService.recordType_resources :
-            dsattributes.kDSStdRecordTypeResources,
-        DirectoryService.recordType_locations :
-            dsattributes.kDSStdRecordTypePlaces,
-    }
-
-    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
-
-    def _uniqueTupleFromAttribute(self, attribute):
-        if attribute:
-            if isinstance(attribute, str):
-                return (attribute,)
-            else:
-                s = set()
-                return tuple([(s.add(x), x)[1] for x in attribute if x not in s])
-        else:
-            return ()
-
-
-    def _setFromAttribute(self, 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 recordsMatchingTokens(self, tokens, context=None, lookupMethod=None):
-        """
-        @param tokens: The tokens to search on
-        @type tokens: C{list} of C{str} (utf-8 bytes)
-        @param context: An indication of what the end user is searching
-            for; "attendee", "location", or None
-        @type context: C{str}
-        @return: a deferred sequence of L{IDirectoryRecord}s which
-            match the given tokens and optional context.
-
-        Each token is searched for within each record's full name and
-        email address; if each token is found within a record that
-        record is returned in the results.
-
-        If context is None, all record types are considered.  If
-        context is "location", only locations are considered.  If
-        context is "attendee", only users, groups, and resources
-        are considered.
-        """
-
-        if lookupMethod is None:
-            lookupMethod = self.odModule.queryRecordsWithAttributes_list
-
-        def collectResults(results):
-            self.log.debug("Got back %d records from OD" % (len(results),))
-            for _ignore_key, value in results:
-                # self.log.debug("OD result: {key} {value}", key=key, value=value)
-                try:
-                    recordNodeName = value.get(
-                        dsattributes.kDSNAttrMetaNodeLocation)
-                    recordShortNames = self._uniqueTupleFromAttribute(
-                        value.get(dsattributes.kDSNAttrRecordName))
-
-                    recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-
-                    recordType = value.get(dsattributes.kDSNAttrRecordType)
-                    if isinstance(recordType, list):
-                        recordType = recordType[0]
-                    if not recordType:
-                        continue
-                    recordType = self._fromODRecordTypes[recordType]
-
-                    # Skip if group restriction is in place and guid is not
-                    # a member (but don't skip any groups)
-                    if (recordType != self.recordType_groups and
-                        self.restrictedGUIDs is not None):
-                        if str(recordGUID) not in self.restrictedGUIDs:
-                            continue
-
-                    recordAuthIDs = self._setFromAttribute(
-                        value.get(dsattributes.kDSNAttrAltSecurityIdentities))
-                    recordFullName = value.get(
-                        dsattributes.kDS1AttrDistinguishedName)
-                    recordFirstName = value.get(dsattributes.kDS1AttrFirstName)
-                    recordLastName = value.get(dsattributes.kDS1AttrLastName)
-                    recordEmailAddresses = self._setFromAttribute(
-                        value.get(dsattributes.kDSNAttrEMailAddress),
-                        lower=True)
-
-                    # Special case for groups, which have members.
-                    if recordType == self.recordType_groups:
-                        memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
-                        if memberGUIDs is None:
-                            memberGUIDs = ()
-                        elif type(memberGUIDs) is str:
-                            memberGUIDs = (memberGUIDs,)
-                        nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
-                        if nestedGUIDs:
-                            if type(nestedGUIDs) is str:
-                                nestedGUIDs = (nestedGUIDs,)
-                            memberGUIDs += tuple(nestedGUIDs)
-                        else:
-                            nestedGUIDs = ()
-                    else:
-                        nestedGUIDs = ()
-                        memberGUIDs = ()
-
-                    # Create records but don't store them in our index or
-                    # send them to memcached, because these are transient,
-                    # existing only so we can create principal resource
-                    # objects that are used to generate the REPORT result.
-
-                    record = OpenDirectoryRecord(
-                        service=self,
-                        recordType=recordType,
-                        guid=recordGUID,
-                        nodeName=recordNodeName,
-                        shortNames=recordShortNames,
-                        authIDs=recordAuthIDs,
-                        fullName=recordFullName,
-                        firstName=recordFirstName,
-                        lastName=recordLastName,
-                        emailAddresses=recordEmailAddresses,
-                        memberGUIDs=memberGUIDs,
-                        nestedGUIDs=nestedGUIDs,
-                        extProxies=(),
-                        extReadOnlyProxies=(),
-                    )
-
-                    # (Copied from below)
-                    # 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.
-                    if self.augmentService is not None:
-                        d = self.augmentService.getAugmentRecord(record.guid,
-                            recordType)
-                        d.addCallback(lambda x: record.addAugmentInformation(x))
-
-                    yield record
-
-                except KeyError:
-                    pass
-
-
-        def multiQuery(directory, queries, recordTypes, attrs):
-            byGUID = {}
-            sets = []
-
-            caseInsensitive = True
-            for compound in queries:
-                compound = compound.generate()
-
-                try:
-                    startTime = time.time()
-                    queryResults = lookupMethod(
-                        directory,
-                        compound,
-                        caseInsensitive,
-                        recordTypes,
-                        attrs,
-                    )
-                    totalTime = time.time() - startTime
-
-                    newSet = set()
-                    for recordName, data in queryResults:
-                        guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
-                        if guid:
-                            byGUID[guid] = (recordName, data)
-                            newSet.add(guid)
-
-                    self.log.debug("Attendee OD query: Types %s, Query %s, %.2f sec, %d results" %
-                        (recordTypes, compound, totalTime, len(queryResults)))
-                    sets.append(newSet)
-
-                except self.odModule.ODError, e:
-                    self.log.error("Ignoring OD Error: %d %s" %
-                        (e.message[1], e.message[0]))
-                    continue
-
-            results = []
-            for guid in set.intersection(*sets):
-                recordName, data = byGUID.get(guid, None)
-                if data is not None:
-                    results.append((data[dsattributes.kDSNAttrRecordName], data))
-            return results
-
-        localQueries = buildLocalQueriesFromTokens(tokens, self._ODFields)
-        nestedQuery = buildNestedQueryFromTokens(tokens, self._ODFields)
-
-        # Starting with the record types corresponding to the context...
-        recordTypes = self.recordTypesForSearchContext(context)
-        # ...limit to the types this service supports...
-        recordTypes = [r for r in recordTypes if r in self.recordTypes()]
-        # ...and map those to OD representations...
-        recordTypes = [self._toODRecordTypes[r] for r in recordTypes]
-
-        if recordTypes:
-            # Perform the complex/nested query.  If there was more than one
-            # token, this won't match anything in /Local, therefore we run
-            # the un-nested queries below and AND the results ourselves in
-            # multiQuery.
-            results = multiQuery(
-                self.directory,
-                [nestedQuery],
-                recordTypes,
-                [
-                    dsattributes.kDS1AttrGeneratedUID,
-                    dsattributes.kDSNAttrRecordName,
-                    dsattributes.kDSNAttrAltSecurityIdentities,
-                    dsattributes.kDSNAttrRecordType,
-                    dsattributes.kDS1AttrDistinguishedName,
-                    dsattributes.kDS1AttrFirstName,
-                    dsattributes.kDS1AttrLastName,
-                    dsattributes.kDSNAttrEMailAddress,
-                    dsattributes.kDSNAttrMetaNodeLocation,
-                    dsattributes.kDSNAttrGroupMembers,
-                    dsattributes.kDSNAttrNestedGroups,
-                ]
-            )
-            if self.localNode is not None and len(tokens) > 1:
-                # /Local is in our search path and the complex query above
-                # would not have matched anything in /Local.  So now run
-                # the un-nested queries.
-                results.extend(
-                    multiQuery(
-                        self.localNode,
-                        localQueries,
-                        recordTypes,
-                        [
-                            dsattributes.kDS1AttrGeneratedUID,
-                            dsattributes.kDSNAttrRecordName,
-                            dsattributes.kDSNAttrAltSecurityIdentities,
-                            dsattributes.kDSNAttrRecordType,
-                            dsattributes.kDS1AttrDistinguishedName,
-                            dsattributes.kDS1AttrFirstName,
-                            dsattributes.kDS1AttrLastName,
-                            dsattributes.kDSNAttrEMailAddress,
-                            dsattributes.kDSNAttrMetaNodeLocation,
-                            dsattributes.kDSNAttrGroupMembers,
-                            dsattributes.kDSNAttrNestedGroups,
-                        ]
-                    )
-                )
-            return succeed(collectResults(results))
-        else:
-            return succeed([])
-
-
-    def recordsMatchingFields(self, fields, operand="or", recordType=None,
-        lookupMethod=None):
-
-        if lookupMethod is None:
-            lookupMethod = self.odModule.queryRecordsWithAttribute_list
-
-        # Note that OD applies case-sensitivity globally across the entire
-        # query, not per expression, so the current code uses whatever is
-        # specified in the last field in the fields list
-
-        def collectResults(results):
-            self.log.debug("Got back %d records from OD" % (len(results),))
-            for _ignore_key, value in results:
-                # self.log.debug("OD result: {key} {value}", key=key, value=value)
-                try:
-                    recordNodeName = value.get(
-                        dsattributes.kDSNAttrMetaNodeLocation)
-                    recordShortNames = self._uniqueTupleFromAttribute(
-                        value.get(dsattributes.kDSNAttrRecordName))
-
-                    recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-
-                    recordType = value.get(dsattributes.kDSNAttrRecordType)
-                    if isinstance(recordType, list):
-                        recordType = recordType[0]
-                    if not recordType:
-                        continue
-                    recordType = self._fromODRecordTypes[recordType]
-
-                    # Skip if group restriction is in place and guid is not
-                    # a member (but don't skip any groups)
-                    if (recordType != self.recordType_groups and
-                        self.restrictedGUIDs is not None):
-                        if str(recordGUID) not in self.restrictedGUIDs:
-                            continue
-
-                    recordAuthIDs = self._setFromAttribute(
-                        value.get(dsattributes.kDSNAttrAltSecurityIdentities))
-                    recordFullName = value.get(
-                        dsattributes.kDS1AttrDistinguishedName)
-                    recordFirstName = value.get(dsattributes.kDS1AttrFirstName)
-                    recordLastName = value.get(dsattributes.kDS1AttrLastName)
-                    recordEmailAddresses = self._setFromAttribute(
-                        value.get(dsattributes.kDSNAttrEMailAddress),
-                        lower=True)
-
-                    # Special case for groups, which have members.
-                    if recordType == self.recordType_groups:
-                        memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
-                        if memberGUIDs is None:
-                            memberGUIDs = ()
-                        elif type(memberGUIDs) is str:
-                            memberGUIDs = (memberGUIDs,)
-                        nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
-                        if nestedGUIDs:
-                            if type(nestedGUIDs) is str:
-                                nestedGUIDs = (nestedGUIDs,)
-                            memberGUIDs += tuple(nestedGUIDs)
-                        else:
-                            nestedGUIDs = ()
-                    else:
-                        nestedGUIDs = ()
-                        memberGUIDs = ()
-
-                    # Create records but don't store them in our index or
-                    # send them to memcached, because these are transient,
-                    # existing only so we can create principal resource
-                    # objects that are used to generate the REPORT result.
-
-                    record = OpenDirectoryRecord(
-                        service=self,
-                        recordType=recordType,
-                        guid=recordGUID,
-                        nodeName=recordNodeName,
-                        shortNames=recordShortNames,
-                        authIDs=recordAuthIDs,
-                        fullName=recordFullName,
-                        firstName=recordFirstName,
-                        lastName=recordLastName,
-                        emailAddresses=recordEmailAddresses,
-                        memberGUIDs=memberGUIDs,
-                        nestedGUIDs=nestedGUIDs,
-                        extProxies=(),
-                        extReadOnlyProxies=(),
-                    )
-
-                    # (Copied from below)
-                    # 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.
-                    if self.augmentService is not None:
-                        d = self.augmentService.getAugmentRecord(record.guid,
-                            recordType)
-                        d.addCallback(lambda x: record.addAugmentInformation(x))
-
-                    yield record
-
-                except KeyError:
-                    pass
-
-
-        def multiQuery(directory, queries, attrs, operand):
-            byGUID = {}
-            sets = []
-
-            for query, recordTypes in queries.iteritems():
-                ODField, value, caseless, matchType = query
-                if matchType == "starts-with":
-                    comparison = dsattributes.eDSStartsWith
-                elif matchType == "contains":
-                    comparison = dsattributes.eDSContains
-                else:
-                    comparison = dsattributes.eDSExact
-
-                self.log.debug("Calling OD: Types %s, Field %s, Value %s, Match %s, Caseless %s" %
-                    (recordTypes, ODField, value, matchType, caseless))
-
-                try:
-                    queryResults = lookupMethod(
-                        directory,
-                        ODField,
-                        value,
-                        comparison,
-                        caseless,
-                        recordTypes,
-                        attrs,
-                    )
-
-                    if operand == dsquery.expression.OR:
-                        for recordName, data in queryResults:
-                            guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
-                            if guid:
-                                byGUID[guid] = (recordName, data)
-                    else: # AND
-                        newSet = set()
-                        for recordName, data in queryResults:
-                            guid = data.get(dsattributes.kDS1AttrGeneratedUID, None)
-                            if guid:
-                                byGUID[guid] = (recordName, data)
-                                newSet.add(guid)
-
-                        sets.append(newSet)
-
-                except self.odModule.ODError, e:
-                    self.log.error("Ignoring OD Error: %d %s" %
-                        (e.message[1], e.message[0]))
-                    continue
-
-            if operand == dsquery.expression.OR:
-                return byGUID.values()
-
-            else:
-                results = []
-                for guid in set.intersection(*sets):
-                    recordName, data = byGUID.get(guid, None)
-                    if data is not None:
-                        results.append((data[dsattributes.kDSNAttrRecordName], data))
-                return results
-
-        operand = (dsquery.expression.OR if operand == "or"
-            else dsquery.expression.AND)
-
-        if recordType is None:
-            # The client is looking for records in any of the four types
-            recordTypes = set(self._toODRecordTypes.values())
-        else:
-            # The client is after only one recordType
-            recordTypes = [self._toODRecordTypes[recordType]]
-
-        queries = buildQueries(recordTypes, fields, self._ODFields)
-
-        results = multiQuery(
-            self.directory,
-            queries,
-            [
-                dsattributes.kDS1AttrGeneratedUID,
-                dsattributes.kDSNAttrRecordName,
-                dsattributes.kDSNAttrAltSecurityIdentities,
-                dsattributes.kDSNAttrRecordType,
-                dsattributes.kDS1AttrDistinguishedName,
-                dsattributes.kDS1AttrFirstName,
-                dsattributes.kDS1AttrLastName,
-                dsattributes.kDSNAttrEMailAddress,
-                dsattributes.kDSNAttrMetaNodeLocation,
-                dsattributes.kDSNAttrGroupMembers,
-                dsattributes.kDSNAttrNestedGroups,
-            ],
-            operand
-        )
-        return succeed(collectResults(results))
-
-
-    def queryDirectory(self, recordTypes, indexType, indexKey,
-        lookupMethod=None):
-
-        if lookupMethod is None:
-            lookupMethod = self.odModule.queryRecordsWithAttribute_list
-
-        origIndexKey = indexKey
-        if indexType == self.INDEX_TYPE_CUA:
-            # The directory doesn't contain CUAs, so we need to convert
-            # the CUA to the appropriate field name and value:
-            queryattr, indexKey = cuAddressConverter(indexKey)
-            # queryattr will be one of:
-            # guid, emailAddresses, or recordName
-            # ...which will need to be mapped to DS
-            queryattr = self._ODFields[queryattr]['odField']
-
-        else:
-            queryattr = {
-                self.INDEX_TYPE_SHORTNAME : dsattributes.kDSNAttrRecordName,
-                self.INDEX_TYPE_GUID      : dsattributes.kDS1AttrGeneratedUID,
-                self.INDEX_TYPE_AUTHID    : dsattributes.kDSNAttrAltSecurityIdentities,
-            }.get(indexType)
-            assert queryattr is not None, "Invalid type for record faulting query"
-        # Make all OD queries case insensitive
-        caseInsensitive = True
-
-        results = []
-        for recordType in recordTypes:
-
-            attrs = [
-                dsattributes.kDS1AttrGeneratedUID,
-                dsattributes.kDSNAttrRecordName,
-                dsattributes.kDSNAttrAltSecurityIdentities,
-                dsattributes.kDSNAttrRecordType,
-                dsattributes.kDS1AttrDistinguishedName,
-                dsattributes.kDS1AttrFirstName,
-                dsattributes.kDS1AttrLastName,
-                dsattributes.kDSNAttrEMailAddress,
-                dsattributes.kDSNAttrMetaNodeLocation,
-            ]
-
-            if recordType == DirectoryService.recordType_users:
-                listRecordTypes = [self._toODRecordTypes[recordType]]
-
-            elif recordType in (
-                DirectoryService.recordType_resources,
-                DirectoryService.recordType_locations,
-            ):
-                if queryattr == dsattributes.kDSNAttrEMailAddress:
-                    continue
-
-                listRecordTypes = [self._toODRecordTypes[recordType]]
-
-            elif recordType == DirectoryService.recordType_groups:
-
-                if queryattr == dsattributes.kDSNAttrEMailAddress:
-                    continue
-
-                listRecordTypes = [dsattributes.kDSStdRecordTypeGroups]
-                attrs.append(dsattributes.kDSNAttrGroupMembers)
-                attrs.append(dsattributes.kDSNAttrNestedGroups)
-
-            else:
-                raise UnknownRecordTypeError("Unknown OpenDirectory record type: %s" % (recordType))
-
-            # Because we're getting transient OD error -14987, try 3 times:
-            for _ignore in xrange(3):
-                try:
-                    self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
-                        self.directory,
-                        queryattr,
-                        indexKey,
-                        dsattributes.eDSExact,
-                        caseInsensitive,
-                        listRecordTypes,
-                        attrs,
-                    ))
-                    lookedUp = lookupMethod(
-                            self.directory,
-                            queryattr,
-                            indexKey,
-                            dsattributes.eDSExact,
-                            caseInsensitive,
-                            listRecordTypes,
-                            attrs,
-                        )
-                    results.extend(lookedUp)
-
-                except self.odModule.ODError, ex:
-                    if ex.message[1] == -14987:
-                        # Fall through and retry
-                        self.log.error("OpenDirectory (node=%s) error: %s" % (self.realmName, str(ex)))
-                    elif ex.message[1] == -14140 or ex.message[1] == -14200:
-                        # Unsupported attribute on record - don't fail
-                        return
-                    else:
-                        self.log.error("OpenDirectory (node=%s) error: %s" % (self.realmName, str(ex)))
-                        raise
-                else:
-                    # Success, so break the retry loop
-                    break
-
-        self.log.debug("opendirectory.queryRecordsWithAttribute_list matched records: %s" % (len(results),))
-
-        enabledRecords = []
-        disabledRecords = []
-
-        for (recordShortName, value) in results:
-
-            # Now get useful record info.
-            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-            recordShortNames = self._uniqueTupleFromAttribute(value.get(dsattributes.kDSNAttrRecordName))
-            recordType = value.get(dsattributes.kDSNAttrRecordType)
-            if isinstance(recordType, list):
-                recordType = recordType[0]
-            recordAuthIDs = self._setFromAttribute(value.get(dsattributes.kDSNAttrAltSecurityIdentities))
-            recordFullName = value.get(dsattributes.kDS1AttrDistinguishedName)
-            recordFirstName = value.get(dsattributes.kDS1AttrFirstName)
-            recordLastName = value.get(dsattributes.kDS1AttrLastName)
-            recordEmailAddresses = self._setFromAttribute(value.get(dsattributes.kDSNAttrEMailAddress), lower=True)
-            recordNodeName = value.get(dsattributes.kDSNAttrMetaNodeLocation)
-
-            if not recordType:
-                self.log.debug("Record (unknown)%s in node %s has no recordType; ignoring."
-                               % (recordShortName, recordNodeName))
-                continue
-
-            recordType = self._fromODRecordTypes[recordType]
-
-            if not recordGUID:
-                self.log.debug("Record (%s)%s in node %s has no GUID; ignoring."
-                               % (recordType, recordShortName, recordNodeName))
-                continue
-
-            if recordGUID.lower().startswith("ffffeeee-dddd-cccc-bbbb-aaaa"):
-                self.log.debug("Ignoring system record (%s)%s in node %s."
-                               % (recordType, recordShortName, recordNodeName))
-                continue
-
-            # If restrictToGroup is in effect, all guids which are not a member
-            # of that group are disabled (overriding the augments db).
-            if (self.restrictedGUIDs is not None):
-                unrestricted = recordGUID in self.restrictedGUIDs
-            else:
-                unrestricted = True
-
-            # Special case for groups, which have members.
-            if recordType == self.recordType_groups:
-                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
-                if memberGUIDs is None:
-                    memberGUIDs = ()
-                elif type(memberGUIDs) is str:
-                    memberGUIDs = (memberGUIDs,)
-                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
-                if nestedGUIDs:
-                    if type(nestedGUIDs) is str:
-                        nestedGUIDs = (nestedGUIDs,)
-                    memberGUIDs += tuple(nestedGUIDs)
-                else:
-                    nestedGUIDs = ()
-            else:
-                memberGUIDs = ()
-                nestedGUIDs = ()
-
-            # 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:
-                    if type(resourceInfo) is not str:
-                        resourceInfo = resourceInfo[0]
-                    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,
-                guid=recordGUID,
-                nodeName=recordNodeName,
-                shortNames=recordShortNames,
-                authIDs=recordAuthIDs,
-                fullName=recordFullName,
-                firstName=recordFirstName,
-                lastName=recordLastName,
-                emailAddresses=recordEmailAddresses,
-                memberGUIDs=memberGUIDs,
-                nestedGUIDs=nestedGUIDs,
-                extProxies=proxyGUIDs,
-                extReadOnlyProxies=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.
-            if self.augmentService is not None:
-                d = self.augmentService.getAugmentRecord(record.guid,
-                    recordType)
-                d.addCallback(lambda x: record.addAugmentInformation(x))
-
-            # Override based on ResourceInfo
-            if autoSchedule:
-                record.autoSchedule = True
-
-            if not unrestricted:
-                self.log.debug("%s is not enabled because it's not a member of group: %s" % (recordGUID, self.restrictToGroup))
-                record.enabledForCalendaring = False
-                record.enabledForAddressBooks = False
-
-            record.applySACLs()
-
-            if record.enabledForCalendaring:
-                enabledRecords.append(record)
-            else:
-                disabledRecords.append(record)
-
-        record = None
-        if len(enabledRecords) == 1:
-            record = enabledRecords[0]
-        elif len(enabledRecords) == 0 and len(disabledRecords) == 1:
-            record = disabledRecords[0]
-        elif indexType == self.INDEX_TYPE_GUID and len(enabledRecords) > 1:
-            self.log.error("Duplicate records found for GUID %s:" % (indexKey,))
-            for duplicateRecord in enabledRecords:
-                self.log.error("Duplicate: %s" % (", ".join(duplicateRecord.shortNames)))
-
-        if record:
-            if isinstance(origIndexKey, unicode):
-                origIndexKey = origIndexKey.encode("utf-8")
-            self.log.debug("Storing (%s %s) %s in internal cache" % (indexType, origIndexKey, record))
-
-            self.recordCacheForType(recordType).addRecord(record, indexType, origIndexKey)
-
-
-    def getResourceInfo(self):
-        """
-        Resource information including proxy assignments for resource and
-        locations, as well as auto-schedule settings, used to live in the
-        directory.  This method fetches old resource info for migration
-        purposes.
-        """
-        attrs = [
-            dsattributes.kDS1AttrGeneratedUID,
-            dsattributes.kDSNAttrResourceInfo,
-        ]
-
-        for recordType in (dsattributes.kDSStdRecordTypePlaces, dsattributes.kDSStdRecordTypeResources):
-            try:
-                self.log.debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                    self.directory,
-                    recordType,
-                    attrs,
-                ))
-                results = self.odModule.listAllRecordsWithAttributes_list(
-                    self.directory,
-                    recordType,
-                    attrs,
-                )
-            except self.odModule.ODError, ex:
-                self.log.error("OpenDirectory (node=%s) error: %s" % (self.realmName, str(ex)))
-                raise
-
-            for (recordShortName, value) in results:
-                recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
-                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
-                if resourceInfo is not None:
-                    if type(resourceInfo) is not str:
-                        resourceInfo = resourceInfo[0]
-                    try:
-                        autoSchedule, proxy, readOnlyProxy = self.parseResourceInfo(resourceInfo,
-                            recordGUID, recordType, recordShortName)
-                    except ValueError:
-                        continue
-                    yield recordGUID, autoSchedule, proxy, readOnlyProxy
-
-
-    def isAvailable(self):
-        """
-        Returns True if all configured directory nodes are accessible, False otherwise
-        """
-
-        if self.node == "/Search":
-            result = self.odModule.getNodeAttributes(self.directory, "/Search",
-                (dsattributes.kDS1AttrSearchPath,))
-            nodes = result[dsattributes.kDS1AttrSearchPath]
-        else:
-            nodes = [self.node]
-
-        try:
-            for node in nodes:
-                self.odModule.getNodeAttributes(self.directory, node, [dsattributes.kDSNAttrNodePath])
-        except self.odModule.ODError:
-            self.log.warn("OpenDirectory Node %s not available" % (node,))
-            return False
-
-        return True
-
-
-    @inlineCallbacks
-    def getGroups(self, guids):
-        """
-        Returns a set of group records for the list of guids passed in.  For
-        any group that also contains subgroups, those subgroups' records are
-        also returned, and so on.
-        """
-
-        recordsByGUID = {}
-        valuesToFetch = guids
-
-        loop = 1
-        while valuesToFetch:
-            self.log.debug("getGroups loop %d" % (loop,))
-
-            results = []
-
-            for batch in splitIntoBatches(valuesToFetch, self.batchSize):
-                fields = []
-                for value in batch:
-                    fields.append(["guid", value, False, "equals"])
-                self.log.debug("getGroups fetching batch of %d" %
-                    (len(fields),))
-                result = list((yield self.recordsMatchingFields(fields,
-                    recordType=self.recordType_groups)))
-                results.extend(result)
-                self.log.debug("getGroups got back batch of %d for subtotal of %d" %
-                    (len(result), len(results)))
-
-            # Reset values for next iteration
-            valuesToFetch = set()
-
-            for record in results:
-                guid = record.guid
-                if guid not in recordsByGUID:
-                    recordsByGUID[guid] = record
-
-                # record.nestedGUIDs() contains the sub groups of this group
-                for memberGUID in record.nestedGUIDs():
-                    if memberGUID not in recordsByGUID:
-                        self.log.debug("getGroups group %s contains group %s" %
-                            (record.guid, memberGUID))
-                        valuesToFetch.add(memberGUID)
-
-            loop += 1
-
-        returnValue(recordsByGUID.values())
-
-
-
-def buildQueries(recordTypes, fields, mapping):
-    """
-    Determine how many queries need to be performed in order to work around opendirectory
-    quirks, where searching on fields that don't apply to a given recordType returns incorrect
-    results (either none, or all records).
-    """
-
-    queries = {}
-    for recordType in recordTypes:
-        for field, value, caseless, matchType in fields:
-            if field in mapping:
-                if recordType in mapping[field]['appliesTo']:
-                    ODField = mapping[field]['odField']
-                    key = (ODField, value, caseless, matchType)
-                    queries.setdefault(key, []).append(recordType)
-
-    return queries
-
-
-
-def buildLocalQueriesFromTokens(tokens, mapping):
-    """
-    OD /Local doesn't support nested complex queries, so create a list of
-    complex queries that will be ANDed together in recordsMatchingTokens()
-
-    @param tokens: The tokens to search on
-    @type tokens: C{list} of C{str}
-    @param mapping: The mapping of DirectoryRecord attributes to OD attributes
-    @type mapping: C{dict}
-    @return: A list of expression objects
-    @type: C{list}
-    """
-
-    if len(tokens) == 0:
-        return None
-
-    fields = [
-        ("fullName", dsattributes.eDSContains),
-        ("emailAddresses", dsattributes.eDSStartsWith),
-    ]
-
-    results = []
-    for token in tokens:
-        queries = []
-        for field, comparison in fields:
-            ODField = mapping[field]['odField']
-            query = dsquery.match(ODField, token, comparison)
-            queries.append(query)
-        results.append(dsquery.expression(dsquery.expression.OR, queries))
-    return results
-
-
-
-def buildNestedQueryFromTokens(tokens, mapping):
-    """
-    Build a DS query espression such that all the tokens must appear in either
-    the fullName (anywhere), emailAddresses (at the beginning) or record name
-    (at the beginning).
-
-    @param tokens: The tokens to search on
-    @type tokens: C{list} of C{str}
-    @param mapping: The mapping of DirectoryRecord attributes to OD attributes
-    @type mapping: C{dict}
-    @return: The nested expression object
-    @type: dsquery.expression
-    """
-
-    if len(tokens) == 0:
-        return None
-
-    fields = [
-        ("fullName", dsattributes.eDSContains),
-        ("emailAddresses", dsattributes.eDSStartsWith),
-        ("recordName", dsattributes.eDSStartsWith),
-    ]
-
-    outer = []
-    for token in tokens:
-        inner = []
-        for field, comparison in fields:
-            ODField = mapping[field]['odField']
-            query = dsquery.match(ODField, token, comparison)
-            inner.append(query)
-        outer.append(dsquery.expression(dsquery.expression.OR, inner))
-    return dsquery.expression(dsquery.expression.AND, outer)
-
-
-
-class OpenDirectoryRecord(CachingDirectoryRecord):
-    """
-    OpenDirectory implementation of L{IDirectoryRecord}.
-    """
-    def __init__(
-        self, service, recordType, guid, nodeName, shortNames, authIDs,
-        fullName, firstName, lastName, emailAddresses, memberGUIDs, nestedGUIDs,
-        extProxies, extReadOnlyProxies,
-    ):
-        super(OpenDirectoryRecord, self).__init__(
-            service=service,
-            recordType=recordType,
-            guid=guid,
-            shortNames=shortNames,
-            authIDs=authIDs,
-            fullName=fullName,
-            firstName=firstName,
-            lastName=lastName,
-            emailAddresses=emailAddresses,
-            extProxies=extProxies,
-            extReadOnlyProxies=extReadOnlyProxies,
-        )
-        self.nodeName = nodeName
-
-        self._memberGUIDs = tuple(memberGUIDs)
-        self._nestedGUIDs = tuple(nestedGUIDs)
-        self._groupMembershipGUIDs = None
-
-
-    def __repr__(self):
-        if self.service.realmName == self.nodeName:
-            location = self.nodeName
-        else:
-            location = "%s->%s" % (self.service.realmName, self.nodeName)
-
-        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
-            self.__class__.__name__,
-            self.recordType,
-            self.service.guid,
-            location,
-            self.guid,
-            ",".join(self.shortNames),
-            self.fullName
-        )
-
-
-    def members(self):
-        if self.recordType != self.service.recordType_groups:
-            return
-
-        for guid in self._memberGUIDs:
-            userRecord = self.service.recordWithGUID(guid)
-            if userRecord is not None:
-                yield userRecord
-
-
-    def groups(self):
-        if self._groupMembershipGUIDs is None:
-            self._groupMembershipGUIDs = self.service.groupsForGUID(self.guid)
-
-        for guid in self._groupMembershipGUIDs:
-            record = self.service.recordWithGUID(guid)
-            if record:
-                yield record
-
-
-    def memberGUIDs(self):
-        return set(self._memberGUIDs)
-
-
-    def nestedGUIDs(self):
-        return set(self._nestedGUIDs)
-
-
-    def verifyCredentials(self, credentials):
-        if isinstance(credentials, UsernamePassword):
-            # Check cached password
-            try:
-                if credentials.password == self.password:
-                    return True
-            except AttributeError:
-                pass
-
-            # Check with directory services
-            try:
-                if self.service.odModule.authenticateUserBasic(self.service.directory, self.nodeName, self.shortNames[0], credentials.password):
-                    # Cache the password to avoid future DS queries
-                    self.password = credentials.password
-                    return True
-            except self.service.odModule.ODError, e:
-                self.log.error("OpenDirectory (node=%s) error while performing basic authentication for user %s: %s"
-                            % (self.service.realmName, self.shortNames[0], e))
-
-            return False
-
-        elif isinstance(credentials, DigestedCredentials):
-            #
-            # We need a special format for the "challenge" and "response" strings passed into OpenDirectory, as it is
-            # picky about exactly what it receives.
-            #
-            try:
-                if "algorithm" not in credentials.fields:
-                    credentials.fields["algorithm"] = "md5"
-                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
-                response = (
-                    'Digest username="%(username)s", '
-                    'realm="%(realm)s", '
-                    'nonce="%(nonce)s", '
-                    'uri="%(uri)s", '
-                    'response="%(response)s",'
-                    'algorithm=%(algorithm)s'
-                ) % credentials.fields
-            except KeyError, e:
-                self.log.error(
-                    "OpenDirectory (node=%s) error while performing digest authentication for user %s: "
-                    "missing digest response field: %s in: %s"
-                    % (self.service.realmName, self.shortNames[0], e, credentials.fields)
-                )
-                return False
-
-            try:
-                if self.digestcache[credentials.fields["uri"]] == response:
-                    return True
-            except (AttributeError, KeyError):
-                pass
-
-            try:
-                if self.service.odModule.authenticateUserDigest(
-                    self.service.directory,
-                    self.nodeName,
-                    self.shortNames[0],
-                    challenge,
-                    response,
-                    credentials.method
-                ):
-                    try:
-                        cache = self.digestcache
-                    except AttributeError:
-                        cache = self.digestcache = {}
-
-                    cache[credentials.fields["uri"]] = response
-
-                    return True
-                else:
-                    self.log.debug(
-"""OpenDirectory digest authentication failed with:
-    Nodename:  %s
-    Username:  %s
-    Challenge: %s
-    Response:  %s
-    Method:    %s
-""" % (self.nodeName, self.shortNames[0], challenge, response,
-       credentials.method))
-
-            except self.service.odModule.ODError, e:
-                self.log.error(
-                    "OpenDirectory (node=%s) error while performing digest authentication for user %s: %s"
-                    % (self.service.realmName, self.shortNames[0], e)
-                )
-                return False
-
-            return False
-
-        return super(OpenDirectoryRecord, self).verifyCredentials(credentials)
-
-
-
-class OpenDirectoryInitError(DirectoryError):
-    """
-    OpenDirectory initialization error.
-    """

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/augment.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/augment.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/augment.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -46,6 +46,7 @@
     "automatic",
 ))
 
+
 class AugmentRecord(object):
     """
     Augmented directory record information
@@ -59,7 +60,7 @@
         enabledForCalendaring=False,
         autoSchedule=False,
         autoScheduleMode="default",
-        autoAcceptGroup="",
+        autoAcceptGroup=None,
         enabledForAddressBooks=False,
         enabledForLogin=True,
     ):
@@ -75,13 +76,15 @@
         self.clonedFromDefault = False
 
 recordTypesMap = {
-    "users" : "User",
-    "groups" : "Group",
-    "locations" : "Location",
-    "resources" : "Resource",
-    "addresses" : "Address",
+    "users": "User",
+    "groups": "Group",
+    "locations": "Location",
+    "resources": "Resource",
+    "addresses": "Address",
+    "wikis": "Wiki",
 }
 
+
 class AugmentDB(object):
     """
     Abstract base class for an augment record database.
@@ -128,7 +131,6 @@
 
         @return: L{Deferred}
         """
-
         recordType = recordTypesMap[recordType]
 
         result = (yield self._lookupAugmentRecord(uid))
@@ -266,9 +268,9 @@
         self.xmlFiles = [fullServerPath(config.DataRoot, path) for path in xmlFiles]
         self.xmlFileStats = {}
         for path in self.xmlFiles:
-            self.xmlFileStats[path] = (0, 0) # mtime, size
+            self.xmlFileStats[path] = (0, 0)  # mtime, size
 
-        self.statSeconds = statSeconds # Don't stat more often than this value
+        self.statSeconds = statSeconds  # Don't stat more often than this value
         self.lastCached = 0
         self.db = {}
 

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/cachingdirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/cachingdirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/cachingdirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,473 +0,0 @@
-# -*- test-case-name: twistedcaldav.directory.test.test_cachedirectory -*-
-##
-# Copyright (c) 2009-2014 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.
-##
-
-"""
-Caching directory service implementation.
-"""
-
-__all__ = [
-    "CachingDirectoryService",
-    "CachingDirectoryRecord",
-    "DictRecordTypeCache",
-]
-
-
-import time
-
-import base64
-
-from twext.python.log import Logger
-
-from twistedcaldav.config import config
-from twistedcaldav.memcacheclient import ClientFactory, MemcacheError
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, DirectoryError, UnknownRecordTypeError
-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.directory.util import normalizeUUID
-
-
-class RecordTypeCache(object):
-    """
-    Abstract class for a record type cache. We will likely have dict and memcache implementations of this.
-    """
-
-    def __init__(self, directoryService, recordType):
-
-        self.directoryService = directoryService
-        self.recordType = recordType
-
-
-    def addRecord(self, record, indexType, indexKey, useMemcache=True,
-        neverExpire=False):
-        raise NotImplementedError()
-
-
-    def removeRecord(self, record):
-        raise NotImplementedError()
-
-
-    def findRecord(self, indexType, indexKey):
-        raise NotImplementedError()
-
-
-
-class DictRecordTypeCache(RecordTypeCache):
-    """
-    Cache implementation using a dict, and uses memcached to share records
-    with other instances.
-    """
-    log = Logger()
-
-    def __init__(self, directoryService, recordType):
-
-        super(DictRecordTypeCache, self).__init__(directoryService, recordType)
-        self.records = set()
-        self.recordsIndexedBy = {
-            CachingDirectoryService.INDEX_TYPE_GUID     : {},
-            CachingDirectoryService.INDEX_TYPE_SHORTNAME: {},
-            CachingDirectoryService.INDEX_TYPE_CUA    : {},
-            CachingDirectoryService.INDEX_TYPE_AUTHID   : {},
-        }
-        self.directoryService = directoryService
-        self.lastPurgedTime = time.time()
-
-
-    def addRecord(self, record, indexType, indexKey, useMemcache=True,
-        neverExpire=False):
-
-        useMemcache = useMemcache and config.Memcached.Pools.Default.ClientEnabled
-        if neverExpire:
-            record.neverExpire()
-
-        self.records.add(record)
-
-        # Also index/cache on guid
-        indexTypes = [(indexType, indexKey)]
-        if indexType != CachingDirectoryService.INDEX_TYPE_GUID:
-            indexTypes.append((CachingDirectoryService.INDEX_TYPE_GUID,
-                record.guid))
-
-        for indexType, indexKey in indexTypes:
-            self.recordsIndexedBy[indexType][indexKey] = record
-            if useMemcache:
-                key = self.directoryService.generateMemcacheKey(indexType, indexKey,
-                    record.recordType)
-                self.log.debug("Memcache: storing %s" % (key,))
-                try:
-                    self.directoryService.memcacheSet(key, record)
-                except DirectoryMemcacheError:
-                    self.log.error("Memcache: failed to store %s" % (key,))
-                    pass
-
-
-    def removeRecord(self, record):
-        if record in self.records:
-            self.records.remove(record)
-            self.log.debug("Removed record %s" % (record.guid,))
-            for indexType in self.directoryService.indexTypes():
-                try:
-                    indexData = getattr(record, CachingDirectoryService.indexTypeToRecordAttribute[indexType])
-                except AttributeError:
-                    continue
-                if isinstance(indexData, basestring):
-                    indexData = [indexData]
-                for item in indexData:
-                    try:
-                        del self.recordsIndexedBy[indexType][item]
-                    except KeyError:
-                        pass
-
-
-    def findRecord(self, indexType, indexKey):
-        self.purgeExpiredRecords()
-        return self.recordsIndexedBy[indexType].get(indexKey)
-
-
-    def purgeExpiredRecords(self):
-        """
-        Scan the cached records and remove any that have expired.
-        Does nothing if we've scanned within the past cacheTimeout seconds.
-        """
-        if time.time() - self.lastPurgedTime > self.directoryService.cacheTimeout:
-            for record in list(self.records):
-                if record.isExpired():
-                    self.removeRecord(record)
-            self.lastPurgedTime = time.time()
-
-
-
-class CachingDirectoryService(DirectoryService):
-    """
-    Caching Directory implementation of L{IDirectoryService}.
-
-    This is class must be overridden to provide a concrete implementation.
-    """
-    log = Logger()
-
-    INDEX_TYPE_GUID = "guid"
-    INDEX_TYPE_SHORTNAME = "shortname"
-    INDEX_TYPE_CUA = "cua"
-    INDEX_TYPE_AUTHID = "authid"
-
-    indexTypeToRecordAttribute = {
-        "guid"     : "guid",
-        "shortname": "shortNames",
-        "cua"      : "calendarUserAddresses",
-        "authid"   : "authIDs",
-    }
-
-    def __init__(
-        self,
-        cacheTimeout=1,
-        negativeCaching=False,
-        cacheClass=DictRecordTypeCache,
-    ):
-        """
-        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
-        """
-
-        self.cacheTimeout = cacheTimeout * 60
-        self.negativeCaching = negativeCaching
-
-        self.cacheClass = cacheClass
-        self._initCaches()
-
-        super(CachingDirectoryService, self).__init__()
-
-
-    def _getMemcacheClient(self, refresh=False):
-        if refresh or not hasattr(self, "memcacheClient"):
-            self.memcacheClient = ClientFactory.getClient(['%s:%s' %
-                (config.Memcached.Pools.Default.BindAddress, config.Memcached.Pools.Default.Port)],
-                debug=0, pickleProtocol=2)
-        return self.memcacheClient
-
-
-    def memcacheSet(self, key, record):
-
-        hideService = isinstance(record, DirectoryRecord)
-
-        try:
-            if hideService:
-                record.service = None # so we don't pickle service
-
-            key = base64.b64encode(key)
-            if not self._getMemcacheClient().set(key, record, time=self.cacheTimeout):
-                self.log.error("Could not write to memcache, retrying")
-                if not self._getMemcacheClient(refresh=True).set(
-                    key, record,
-                    time=self.cacheTimeout
-                ):
-                    self.log.error("Could not write to memcache again, giving up")
-                    del self.memcacheClient
-                    raise DirectoryMemcacheError("Failed to write to memcache")
-        finally:
-            if hideService:
-                record.service = self
-
-
-    def memcacheGet(self, key):
-
-        key = base64.b64encode(key)
-        try:
-            record = self._getMemcacheClient().get(key)
-            if record is not None and isinstance(record, DirectoryRecord):
-                record.service = self
-        except MemcacheError:
-            self.log.error("Could not read from memcache, retrying")
-            try:
-                record = self._getMemcacheClient(refresh=True).get(key)
-                if record is not None and isinstance(record, DirectoryRecord):
-                    record.service = self
-            except MemcacheError:
-                self.log.error("Could not read from memcache again, giving up")
-                del self.memcacheClient
-                raise DirectoryMemcacheError("Failed to read from memcache")
-        return record
-
-
-    def generateMemcacheKey(self, indexType, indexKey, recordType):
-        """
-        Return a key that can be used to store/retrieve a record in memcache.
-        if short-name is the indexType the recordType be encoded into the key.
-
-        @param indexType: one of the indexTypes( ) values
-        @type indexType: C{str}
-        @param indexKey: the value being indexed
-        @type indexKey: C{str}
-        @param recordType: the type of record being cached
-        @type recordType: C{str}
-        @return: a memcache key comprised of the passed-in values and the directory
-            service's baseGUID
-        @rtype: C{str}
-        """
-        keyVersion = 2
-        if indexType == CachingDirectoryService.INDEX_TYPE_SHORTNAME:
-            return "dir|v%d|%s|%s|%s|%s" % (keyVersion, self.baseGUID, recordType,
-                indexType, indexKey)
-        else:
-            return "dir|v%d|%s|%s|%s" % (keyVersion, self.baseGUID, indexType,
-                indexKey)
-
-
-    def _initCaches(self):
-        self._recordCaches = dict([
-            (recordType, self.cacheClass(self, recordType))
-            for recordType in self.recordTypes()
-        ])
-
-        self._disabledKeys = dict([(indexType, dict()) for indexType in self.indexTypes()])
-
-
-    def indexTypes(self):
-
-        return (
-            CachingDirectoryService.INDEX_TYPE_GUID,
-            CachingDirectoryService.INDEX_TYPE_SHORTNAME,
-            CachingDirectoryService.INDEX_TYPE_CUA,
-            CachingDirectoryService.INDEX_TYPE_AUTHID,
-        )
-
-
-    def recordCacheForType(self, recordType):
-        try:
-            return self._recordCaches[recordType]
-        except KeyError:
-            raise UnknownRecordTypeError(recordType)
-
-
-    def listRecords(self, recordType):
-        return self.recordCacheForType(recordType).records
-
-
-    def recordWithShortName(self, recordType, shortName):
-        return self._lookupRecord((recordType,), CachingDirectoryService.INDEX_TYPE_SHORTNAME, shortName)
-
-
-    def recordWithCalendarUserAddress(self, address):
-        address = normalizeCUAddr(address)
-        record = None
-        if address.startswith("mailto:"):
-            record = self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_CUA, address)
-            return record if record and record.enabledForCalendaring else None
-        else:
-            return DirectoryService.recordWithCalendarUserAddress(self, address)
-
-
-    def recordWithAuthID(self, authID):
-        return self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_AUTHID, authID)
-
-
-    def recordWithGUID(self, guid):
-        guid = normalizeUUID(guid)
-        return self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_GUID, guid)
-
-    recordWithUID = recordWithGUID
-
-    def _lookupRecord(self, recordTypes, indexType, indexKey):
-
-        if recordTypes is None:
-            recordTypes = self.recordTypes()
-        else:
-            # Only use recordTypes this service supports:
-            supportedRecordTypes = self.recordTypes()
-            recordTypes = [t for t in recordTypes if t in supportedRecordTypes]
-            if not recordTypes:
-                return None
-
-        def lookup():
-            for recordType in recordTypes:
-                record = self.recordCacheForType(recordType).findRecord(indexType, indexKey)
-
-                if record:
-                    if record.isExpired():
-                        self.recordCacheForType(recordType).removeRecord(record)
-                        return None
-                    else:
-                        return record
-            else:
-                return None
-
-        record = lookup()
-        if record:
-            return record
-
-        if self.negativeCaching:
-
-            # Check negative cache (take cache entry timeout into account)
-            try:
-                disabledTime = self._disabledKeys[indexType][indexKey]
-                if time.time() - disabledTime < self.cacheTimeout:
-                    return None
-            except KeyError:
-                pass
-
-        # Check memcache
-        if config.Memcached.Pools.Default.ClientEnabled:
-
-            # The only time the recordType arg matters is when indexType is
-            # short-name, and in that case recordTypes will contain exactly
-            # one recordType, so using recordTypes[0] here is always safe:
-            key = self.generateMemcacheKey(indexType, indexKey, recordTypes[0])
-
-            self.log.debug("Memcache: checking %s" % (key,))
-
-            try:
-                record = self.memcacheGet(key)
-            except DirectoryMemcacheError:
-                self.log.error("Memcache: failed to get %s" % (key,))
-                record = None
-
-            if record is None:
-                self.log.debug("Memcache: miss %s" % (key,))
-            else:
-                self.log.debug("Memcache: hit %s" % (key,))
-                self.recordCacheForType(record.recordType).addRecord(record, indexType, indexKey, useMemcache=False)
-                return record
-
-            if self.negativeCaching:
-
-                # Check negative memcache
-                try:
-                    val = self.memcacheGet("-%s" % (key,))
-                except DirectoryMemcacheError:
-                    self.log.error("Memcache: failed to get -%s" % (key,))
-                    val = None
-                if val == 1:
-                    self.log.debug("Memcache: negative %s" % (key,))
-                    self._disabledKeys[indexType][indexKey] = time.time()
-                    return None
-
-        # Try query
-        self.log.debug("Faulting record for attribute '%s' with value '%s'" % (indexType, indexKey,))
-        self.queryDirectory(recordTypes, indexType, indexKey)
-
-        # Now try again from cache
-        record = lookup()
-        if record:
-            self.log.debug("Found record for attribute '%s' with value '%s'" % (indexType, indexKey,))
-            return record
-
-        if self.negativeCaching:
-
-            # Add to negative cache with timestamp
-            self.log.debug("Failed to fault record for attribute '%s' with value '%s'" % (indexType, indexKey,))
-            self._disabledKeys[indexType][indexKey] = time.time()
-
-            if config.Memcached.Pools.Default.ClientEnabled:
-                self.log.debug("Memcache: storing (negative) %s" % (key,))
-                try:
-                    self.memcacheSet("-%s" % (key,), 1)
-                except DirectoryMemcacheError:
-                    self.log.error("Memcache: failed to set -%s" % (key,))
-                    pass
-
-        return None
-
-
-    def queryDirectory(self, recordTypes, indexType, indexKey):
-        raise NotImplementedError()
-
-
-
-class CachingDirectoryRecord(DirectoryRecord):
-
-    def __init__(
-        self, service, recordType, guid,
-        shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, **kwargs
-    ):
-        super(CachingDirectoryRecord, self).__init__(
-            service,
-            recordType,
-            guid,
-            shortNames=shortNames,
-            authIDs=authIDs,
-            fullName=fullName,
-            firstName=firstName,
-            lastName=lastName,
-            emailAddresses=emailAddresses,
-            uid=uid,
-            **kwargs
-        )
-
-        self.cachedTime = time.time()
-
-
-    def neverExpire(self):
-        self.cachedTime = 0
-
-
-    def isExpired(self):
-        """
-        Returns True if this record was created more than cacheTimeout
-        seconds ago
-        """
-        if (
-            self.cachedTime != 0 and
-            time.time() - self.cachedTime > self.service.cacheTimeout
-        ):
-            return True
-        else:
-            return False
-
-
-
-class DirectoryMemcacheError(DirectoryError):
-    """
-    Error communicating with memcached.
-    """

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendar.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendar.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -35,11 +35,10 @@
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 
 from twistedcaldav.config import config
-from twistedcaldav.directory.idirectory import IDirectoryService
 from twistedcaldav.directory.common import uidsResourceName, \
     CommonUIDProvisioningResource, CommonHomeTypeProvisioningResource
 
-from twistedcaldav.directory.wiki import getWikiACL
+from txdav.who.wiki import getWikiACL
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource, \
     DAVResourceWithChildrenMixin
 from twistedcaldav.resource import CalendarHomeResource
@@ -48,7 +47,10 @@
 
 log = Logger()
 
+
 # FIXME: copied from resource.py to avoid circular dependency
+
+
 class CalDAVComplianceMixIn(object):
     def davComplianceClasses(self):
         return (
@@ -65,7 +67,7 @@
     DAVResource,
 ):
     def defaultAccessControlList(self):
-        return config.ProvisioningResourceACL
+        return succeed(config.ProvisioningResourceACL)
 
 
     def etag(self):
@@ -91,7 +93,8 @@
 
         super(DirectoryCalendarHomeProvisioningResource, self).__init__()
 
-        self.directory = IDirectoryService(directory)
+        # MOVE2WHO
+        self.directory = directory  # IDirectoryService(directory)
         self._url = url
         self._newStore = store
 
@@ -101,9 +104,27 @@
         #
         # Create children
         #
-        for recordType in self.directory.recordTypes():
-            self.putChild(recordType, DirectoryCalendarHomeTypeProvisioningResource(self, recordType))
+        # ...just users, locations, and resources though.  If we iterate all of
+        # the directory's recordTypes, we also get the proxy sub principal types
+        # and other things which don't have calendars.
 
+        self.supportedChildTypes = (
+            self.directory.recordType.user,
+            self.directory.recordType.location,
+            self.directory.recordType.resource,
+        )
+
+        for recordType, recordTypeName in [
+            (r, self.directory.recordTypeToOldName(r)) for r in
+            self.supportedChildTypes
+        ]:
+            self.putChild(
+                recordTypeName,
+                DirectoryCalendarHomeTypeProvisioningResource(
+                    self, recordTypeName, recordType
+                )
+            )
+
         self.putChild(uidsResourceName, DirectoryCalendarHomeUIDProvisioningResource(self))
 
 
@@ -112,7 +133,10 @@
 
 
     def listChildren(self):
-        return self.directory.recordTypes()
+        return [
+            self.directory.recordTypeToOldName(r) for r in
+            self.supportedChildTypes
+        ]
 
 
     def principalCollections(self):
@@ -127,12 +151,13 @@
         return self.directory.principalCollection.principalForRecord(record)
 
 
+    @inlineCallbacks
     def homeForDirectoryRecord(self, record, request):
-        uidResource = self.getChild(uidsResourceName)
+        uidResource = yield self.getChild(uidsResourceName)
         if uidResource is None:
-            return None
+            returnValue(None)
         else:
-            return uidResource.homeResourceForRecord(record, request)
+            returnValue((yield uidResource.homeResourceForRecord(record, request)))
 
 
     ##
@@ -149,42 +174,43 @@
 
 
 class DirectoryCalendarHomeTypeProvisioningResource(
-        CommonHomeTypeProvisioningResource,
-        DirectoryCalendarProvisioningResource
-    ):
+    CommonHomeTypeProvisioningResource,
+    DirectoryCalendarProvisioningResource
+):
     """
     Resource which provisions calendar home collections of a specific
     record type as needed.
     """
-    def __init__(self, parent, recordType):
+    def __init__(self, parent, name, recordType):
         """
         @param parent: the parent of this resource
         @param recordType: the directory record type to provision.
         """
         assert parent is not None
+        assert name is not None
         assert recordType is not None
 
         super(DirectoryCalendarHomeTypeProvisioningResource, self).__init__()
 
         self.directory = parent.directory
+        self.name = name
         self.recordType = recordType
         self._parent = parent
 
 
     def url(self):
-        return joinURL(self._parent.url(), self.recordType)
+        return joinURL(self._parent.url(), self.name)
 
 
+    @inlineCallbacks
     def listChildren(self):
         if config.EnablePrincipalListings:
-
-            def _recordShortnameExpand():
-                for record in self.directory.listRecords(self.recordType):
-                    if record.enabledForCalendaring:
-                        for shortName in record.shortNames:
-                            yield shortName
-
-            return _recordShortnameExpand()
+            children = []
+            for record in (yield self.directory.recordsWithRecordType(self.recordType)):
+                if record.hasCalendars:
+                    for shortName in record.shortNames:
+                        children.append(shortName)
+            returnValue(children)
         else:
             # Not a listable collection
             raise HTTPError(responsecode.FORBIDDEN)
@@ -203,7 +229,7 @@
 
 
     def displayName(self):
-        return self.recordType
+        return self.name
 
 
     ##
@@ -220,13 +246,13 @@
 
 
 class DirectoryCalendarHomeUIDProvisioningResource (
-        CommonUIDProvisioningResource,
-        DirectoryCalendarProvisioningResource
-    ):
+    CommonUIDProvisioningResource,
+    DirectoryCalendarProvisioningResource
+):
 
     homeResourceTypeName = 'calendars'
 
-    enabledAttribute = 'enabledForCalendaring'
+    enabledAttribute = 'hasCalendars'
 
     def homeResourceCreator(self, record, transaction):
         return DirectoryCalendarHomeResource.createHomeResource(
@@ -258,7 +284,7 @@
             else:
                 # ...otherwise permissions are fixed, and are not subject to
                 # inheritance rules, etc.
-                return succeed(self.defaultAccessControlList())
+                return self.defaultAccessControlList()
 
         d = getWikiACL(self, request)
         d.addCallback(gotACL)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxy.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxy.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -29,37 +29,41 @@
 
 import itertools
 import time
+import uuid
 
+from twext.python.log import Logger
+from twext.who.idirectory import RecordType as BaseRecordType
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-from txweb2 import responsecode
-from txweb2.http import HTTPError, StatusResponse
-from txdav.xml import element as davxml
-from txdav.xml.base import dav_namespace
-from txweb2.dav.util import joinURL
-from txweb2.dav.noneprops import NonePropertyStore
+from twisted.python.modules import getModule
+from twisted.web.template import XMLFile, Element, renderer
+from twistedcaldav.config import config, fullServerPath
+from twistedcaldav.database import (
+    AbstractADBAPIDatabase, ADBAPISqliteMixin, ADBAPIPostgreSQLMixin
+)
+from twistedcaldav.directory.util import normalizeUUID
+from twistedcaldav.directory.util import (
+    formatLink, formatLinks, formatPrincipals
+)
 
-from twext.python.log import Logger
-
-from twisted.web.template import XMLFile, Element, renderer
-from twisted.python.modules import getModule
+from twistedcaldav.extensions import (
+    DAVPrincipalResource, DAVResourceWithChildrenMixin
+)
 from twistedcaldav.extensions import DirectoryElement
-from twistedcaldav.directory.principal import formatLink
-from twistedcaldav.directory.principal import formatLinks
-from twistedcaldav.directory.principal import formatPrincipals
-
-from twistedcaldav.directory.util import normalizeUUID
-from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.database import AbstractADBAPIDatabase, ADBAPISqliteMixin, \
-    ADBAPIPostgreSQLMixin
-from twistedcaldav.extensions import DAVPrincipalResource, \
-    DAVResourceWithChildrenMixin
 from twistedcaldav.extensions import ReadOnlyWritePropertiesResourceMixIn
 from twistedcaldav.memcacher import Memcacher
 from twistedcaldav.resource import CalDAVComplianceMixIn
+from txdav.who.delegates import RecordType as DelegateRecordType
+from txdav.xml import element as davxml
+from txdav.xml.base import dav_namespace
+from txweb2 import responsecode
+from txweb2.dav.noneprops import NonePropertyStore
+from txweb2.dav.util import joinURL
+from txweb2.http import HTTPError, StatusResponse
 
 thisModule = getModule(__name__)
 log = Logger()
 
+
 class PermissionsMixIn (ReadOnlyWritePropertiesResourceMixIn):
     def defaultAccessControlList(self):
         aces = (
@@ -86,13 +90,13 @@
             for principal in config.AdminPrincipals
         ))
 
-        return davxml.ACL(*aces)
+        return succeed(davxml.ACL(*aces))
 
 
     def accessControlList(self, request, inheritance=True, expanding=False,
                           inherited_aces=None):
         # Permissions here are fixed, and are not subject to inheritance rules, etc.
-        return succeed(self.defaultAccessControlList())
+        return self.defaultAccessControlList()
 
 
 
@@ -119,13 +123,20 @@
         record = self.resource.parent.record
         resource = self.resource
         parent = self.resource.parent
+        try:
+            if isinstance(record.guid, uuid.UUID):
+                guid = str(record.guid).upper()
+            else:
+                guid = record.guid
+        except AttributeError:
+            guid = ""
         return tag.fillSlots(
             directoryGUID=record.service.guid,
             realm=record.service.realmName,
-            guid=record.guid,
-            recordType=record.recordType,
+            guid=guid,
+            recordType=record.service.recordTypeToOldName(record.recordType),
             shortNames=record.shortNames,
-            fullName=record.fullName,
+            fullName=record.displayName,
             principalUID=parent.principalUID(),
             principalURL=formatLink(parent.principalURL()),
             proxyPrincipalUID=resource.principalUID(),
@@ -209,9 +220,13 @@
 
     def resourceType(self):
         if self.proxyType == "calendar-proxy-read":
-            return davxml.ResourceType.calendarproxyread #@UndefinedVariable
+            return davxml.ResourceType.calendarproxyread  # @UndefinedVariable
         elif self.proxyType == "calendar-proxy-write":
-            return davxml.ResourceType.calendarproxywrite #@UndefinedVariable
+            return davxml.ResourceType.calendarproxywrite  # @UndefinedVariable
+        elif self.proxyType == "calendar-proxy-read-for":
+            return davxml.ResourceType.calendarproxyreadfor  # @UndefinedVariable
+        elif self.proxyType == "calendar-proxy-write-for":
+            return davxml.ResourceType.calendarproxywritefor  # @UndefinedVariable
         else:
             return super(CalendarUserProxyPrincipalResource, self).resourceType()
 
@@ -270,7 +285,7 @@
         principals = []
         newUIDs = set()
         for uri in members:
-            principal = self.pcollection._principalForURI(uri)
+            principal = yield self.pcollection._principalForURI(uri)
             # Invalid principals MUST result in an error.
             if principal is None or principal.principalURL() != uri:
                 raise HTTPError(StatusResponse(
@@ -282,7 +297,9 @@
             newUIDs.add(principal.principalUID())
 
         # Get the old set of UIDs
-        oldUIDs = (yield self._index().getMembers(self.uid))
+        # oldUIDs = (yield self._index().getMembers(self.uid))
+        oldPrincipals = yield self.groupMembers()
+        oldUIDs = [p.principalUID() for p in oldPrincipals]
 
         # Change membership
         yield self.setGroupMemberSetPrincipals(principals)
@@ -293,19 +310,24 @@
 
         changedUIDs = newUIDs.symmetric_difference(oldUIDs)
         for uid in changedUIDs:
-            principal = self.pcollection.principalForUID(uid)
+            principal = yield self.pcollection.principalForUID(uid)
             if principal:
                 yield principal.cacheNotifier.changed()
 
         returnValue(True)
 
 
+    @inlineCallbacks
     def setGroupMemberSetPrincipals(self, principals):
-        # Map the principals to UIDs.
-        return self._index().setGroupMembers(
-            self.uid,
-            [p.principalUID() for p in principals],
+
+        # Find our pseudo-record
+        record = yield self.parent.record.service.recordWithShortName(
+            self._recordTypeFromProxyType(),
+            self.parent.principalUID()
         )
+        # Set the members
+        memberRecords = [p.record for p in principals]
+        yield record.setMembers(memberRecords)
 
 
     ##
@@ -349,7 +371,7 @@
 
 
     @inlineCallbacks
-    def _expandMemberUIDs(self, uid=None, relatives=None, uids=None, infinity=False):
+    def _expandMemberPrincipals(self, uid=None, relatives=None, uids=None, infinity=False):
         if uid is None:
             uid = self.principalUID()
         if relatives is None:
@@ -360,14 +382,14 @@
         if uid not in uids:
             from twistedcaldav.directory.principal import DirectoryPrincipalResource
             uids.add(uid)
-            principal = self.pcollection.principalForUID(uid)
+            principal = yield self.pcollection.principalForUID(uid)
             if isinstance(principal, CalendarUserProxyPrincipalResource):
                 members = yield self._directGroupMembers()
                 for member in members:
                     if member.principalUID() not in uids:
                         relatives.add(member)
                         if infinity:
-                            yield self._expandMemberUIDs(member.principalUID(), relatives, uids, infinity=infinity)
+                            yield self._expandMemberPrincipals(member.principalUID(), relatives, uids, infinity=infinity)
             elif isinstance(principal, DirectoryPrincipalResource):
                 if infinity:
                     members = yield principal.expandedGroupMembers()
@@ -378,30 +400,45 @@
         returnValue(relatives)
 
 
+    def _recordTypeFromProxyType(self):
+        return {
+            "calendar-proxy-read": DelegateRecordType.readDelegateGroup,
+            "calendar-proxy-write": DelegateRecordType.writeDelegateGroup,
+            "calendar-proxy-read-for": DelegateRecordType.readDelegatorGroup,
+            "calendar-proxy-write-for": DelegateRecordType.writeDelegatorGroup,
+        }.get(self.proxyType)
+
+
     @inlineCallbacks
     def _directGroupMembers(self):
-        # Get member UIDs from database and map to principal resources
-        members = yield self._index().getMembers(self.uid)
-        found = []
-        for uid in members:
-            p = self.pcollection.principalForUID(uid)
-            if p:
-                # Only principals enabledForLogin can be a delegate
-                # (and groups as well)
-                if (p.record.enabledForLogin or
-                    p.record.recordType == p.record.service.recordType_groups):
-                    found.append(p)
-                # Make sure any outstanding deletion timer entries for
-                # existing principals are removed
-                yield self._index().refreshPrincipal(uid)
-            else:
-                self.log.warn("Delegate is missing from directory: %s" % (uid,))
+        """
+        Fault in the record representing the sub principal for this proxy type
+        (either read-only or read-write), then fault in the direct members of
+        that record.
+        """
+        memberPrincipals = []
+        record = yield self.parent.record.service.recordWithShortName(
+            self._recordTypeFromProxyType(),
+            self.parent.principalUID()
+        )
+        if record is not None:
+            memberRecords = yield record.members()
+            for record in memberRecords:
+                if record is not None:
+                    principal = yield self.pcollection.principalForRecord(
+                        record
+                    )
+                    if principal is not None:
+                        if (
+                            principal.record.loginAllowed or
+                            principal.record.recordType is BaseRecordType.group
+                        ):
+                            memberPrincipals.append(principal)
+        returnValue(memberPrincipals)
 
-        returnValue(found)
 
-
     def groupMembers(self):
-        return self._expandMemberUIDs()
+        return self._expandMemberPrincipals()
 
 
     @inlineCallbacks
@@ -410,18 +447,12 @@
         Return the complete, flattened set of principals belonging to this
         group.
         """
-        returnValue((yield self._expandMemberUIDs(infinity=True)))
+        returnValue((yield self._expandMemberPrincipals(infinity=True)))
 
 
     def groupMemberships(self):
-        # Get membership UIDs and map to principal resources
-        d = self._index().getMemberships(self.uid)
-        d.addCallback(lambda memberships: [
-            p for p
-            in [self.pcollection.principalForUID(uid) for uid in memberships]
-            if p
-        ])
-        return d
+        # Unlikely to ever want to put a subprincipal into a group
+        return succeed([])
 
 
     @inlineCallbacks
@@ -437,7 +468,7 @@
         @return: True if principal is a proxy (of the correct type) of our parent
         @rtype: C{boolean}
         """
-        readWrite = self.isProxyType(True) # is read-write
+        readWrite = self.isProxyType(True)  # is read-write
         if principal and self.parent in (yield principal.proxyFor(readWrite)):
             returnValue(True)
         returnValue(False)
@@ -630,7 +661,7 @@
 
             overdue = yield self._memcacher.checkDeletionTimer(principalUID)
 
-            if overdue == False:
+            if overdue is False:
                 # Do nothing
                 returnValue(None)
 
@@ -855,9 +886,9 @@
         )
         if alreadyDone is None:
             for (groupname, member) in (
-                    (yield self._db_all_values_for_sql(
-                        "select GROUPNAME, MEMBER from GROUPS"))
-                ):
+                (yield self._db_all_values_for_sql(
+                    "select GROUPNAME, MEMBER from GROUPS"))
+            ):
                 grouplist = groupname.split("#")
                 grouplist[0] = normalizeUUID(grouplist[0])
                 newGroupName = "#".join(grouplist)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxyloader.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxyloader.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/calendaruserproxyloader.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -29,7 +29,6 @@
 from twext.python.log import Logger
 
 from twistedcaldav.config import config, fullServerPath
-from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.xmlutil import readXML
 
 log = Logger()
@@ -44,6 +43,7 @@
 
 ATTRIBUTE_REPEAT = "repeat"
 
+
 class XMLCalendarUserProxyLoader(object):
     """
     XML calendar user proxy configuration file parser and loader.
@@ -52,10 +52,11 @@
         return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
 
 
-    def __init__(self, xmlFile):
+    def __init__(self, xmlFile, service):
 
         self.items = []
         self.xmlFile = fullServerPath(config.DataRoot, xmlFile)
+        self.service = service
 
         # Read in XML
         try:
@@ -131,7 +132,7 @@
     @inlineCallbacks
     def updateProxyDB(self):
 
-        db = calendaruserproxy.ProxyDBService
+        db = self.service
         for item in self.items:
             guid, write_proxies, read_proxies = item
             yield db.setGroupMembers("%s#%s" % (guid, "calendar-proxy-write"), write_proxies)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/common.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/common.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/common.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -37,6 +37,7 @@
 
 uidsResourceName = "__uids__"
 
+
 class CommonUIDProvisioningResource(object):
     """
     Common ancestor for addressbook/calendar UID provisioning resources.
@@ -68,10 +69,10 @@
         name = record.uid
 
         if record is None:
-            log.debug("No directory record with GUID %r" % (name,))
+            log.debug("No directory record with UID %r" % (name,))
             returnValue(None)
 
-        if not getattr(record, self.enabledAttribute):
+        if not getattr(record, self.enabledAttribute, False):
             log.debug("Directory record %r is not enabled for %s" % (
                 record, self.homeResourceTypeName))
             returnValue(None)
@@ -94,7 +95,7 @@
         if name == "":
             returnValue((self, ()))
 
-        record = self.directory.recordWithUID(name)
+        record = yield self.directory.recordWithUID(name)
         if record:
             child = yield self.homeResourceForRecord(record, request)
             returnValue((child, segments[1:]))
@@ -149,7 +150,7 @@
         if name == "":
             returnValue((self, segments[1:]))
 
-        record = self.directory.recordWithShortName(self.recordType, name)
+        record = yield self.directory.recordWithShortName(self.recordType, name)
         if record is None:
             returnValue(
                 (NotFoundResource(principalCollections=self._parent.principalCollections()), [])

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory-principal-resource.html
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory-principal-resource.html	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory-principal-resource.html	2014-04-04 17:20:27 UTC (rev 13158)
@@ -11,10 +11,7 @@
 GUID: <t:slot name="principalGUID"/>
 Record type: <t:slot name="recordType"/>
 Short names: <t:slot name="shortNames"/>
-Security Identities: <t:slot name="securityIDs"/>
 Full name: <t:slot name="fullName"/>
-First name: <t:slot name="firstName"/>
-Last name: <t:slot name="lastName"/>
 Email addresses:
 <t:slot name="emailAddresses" />Principal UID: <t:slot name="principalUID"/>
 Principal URL: <t:slot name="principalURL"/>

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/directory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,1509 +0,0 @@
-# -*- test-case-name: twistedcaldav.directory.test -*-
-##
-# Copyright (c) 2006-2014 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.
-##
-
-
-"""
-Generic directory service classes.
-"""
-
-__all__ = [
-    "DirectoryService",
-    "DirectoryRecord",
-    "DirectoryError",
-    "DirectoryConfigurationError",
-    "UnknownRecordTypeError",
-    "GroupMembershipCacheUpdater",
-]
-
-from plistlib import readPlistFromString
-
-from twext.python.log import Logger
-from txweb2.dav.auth import IPrincipalCredentials
-from txweb2.dav.util import joinURL
-
-from twisted.cred.checkers import ICredentialsChecker
-from twisted.cred.error import UnauthorizedLogin
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.config import config
-from twistedcaldav.directory.idirectory import IDirectoryService, IDirectoryRecord
-from twistedcaldav.directory.util import uuidFromName, normalizeUUID
-from twistedcaldav.memcacher import Memcacher
-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
-from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
-
-from txdav.caldav.icalendardirectoryservice import ICalendarStoreDirectoryService, \
-    ICalendarStoreDirectoryRecord
-
-from xml.parsers.expat import ExpatError
-
-from zope.interface import implements
-
-import cPickle as pickle
-import datetime
-import grp
-import itertools
-import os
-import pwd
-import sys
-import types
-from urllib import unquote
-
-log = Logger()
-
-
-class DirectoryService(object):
-    implements(IDirectoryService, ICalendarStoreDirectoryService, ICredentialsChecker)
-
-    log = Logger()
-
-    ##
-    # IDirectoryService
-    ##
-
-    realmName = None
-
-    recordType_users = "users"
-    recordType_people = "people"
-    recordType_groups = "groups"
-    recordType_locations = "locations"
-    recordType_resources = "resources"
-    recordType_addresses = "addresses"
-
-    searchContext_location = "location"
-    searchContext_resource = "resource"
-    searchContext_user = "user"
-    searchContext_group = "group"
-    searchContext_attendee = "attendee"
-
-    aggregateService = None
-
-    def _generatedGUID(self):
-        if not hasattr(self, "_guid"):
-            realmName = self.realmName
-
-            assert self.baseGUID, "Class %s must provide a baseGUID attribute" % (self.__class__.__name__,)
-
-            if realmName is None:
-                self.log.error("Directory service %s has no realm name or GUID; generated service GUID will not be unique." % (self,))
-                realmName = ""
-            else:
-                self.log.info("Directory service %s has no GUID; generating service GUID from realm name." % (self,))
-
-            self._guid = uuidFromName(self.baseGUID, realmName)
-
-        return self._guid
-
-    baseGUID = None
-    guid = property(_generatedGUID)
-
-    # Needed by twistedcaldav.directorybackedaddressbook
-    liveQuery = False
-
-    def setRealm(self, realmName):
-        self.realmName = realmName
-
-
-    def available(self):
-        """
-        By default, the directory is available.  This may return a boolean or a
-        Deferred which fires a boolean.
-
-        A return value of "False" means that the directory is currently
-        unavailable due to the service starting up.
-        """
-        return True
-    # end directorybackedaddressbook requirements
-
-    ##
-    # ICredentialsChecker
-    ##
-
-    # For ICredentialsChecker
-    credentialInterfaces = (IPrincipalCredentials,)
-
-    def requestAvatarId(self, credentials):
-        credentials = IPrincipalCredentials(credentials)
-
-        # FIXME: ?
-        # We were checking if principal is enabled; seems unnecessary in current
-        # implementation because you shouldn't have a principal object for a
-        # disabled directory principal.
-
-        if credentials.authnPrincipal is None:
-            raise UnauthorizedLogin("No such user: %s" % (credentials.credentials.username,))
-
-        # See if record is enabledForLogin
-        if not credentials.authnPrincipal.record.isLoginEnabled():
-            raise UnauthorizedLogin("User not allowed to log in: %s" %
-                (credentials.credentials.username,))
-
-        # Handle Kerberos as a separate behavior
-        try:
-            from twistedcaldav.authkerb import NegotiateCredentials
-        except ImportError:
-            NegotiateCredentials = None
-
-        if NegotiateCredentials and isinstance(credentials.credentials,
-                                               NegotiateCredentials):
-            # If we get here with Kerberos, then authentication has already succeeded
-            return (
-                credentials.authnPrincipal.principalURL(),
-                credentials.authzPrincipal.principalURL(),
-                credentials.authnPrincipal,
-                credentials.authzPrincipal,
-            )
-        else:
-            if credentials.authnPrincipal.record.verifyCredentials(credentials.credentials):
-                return (
-                    credentials.authnPrincipal.principalURL(),
-                    credentials.authzPrincipal.principalURL(),
-                    credentials.authnPrincipal,
-                    credentials.authzPrincipal,
-                )
-            else:
-                raise UnauthorizedLogin("Incorrect credentials for %s" % (credentials.credentials.username,))
-
-
-    def recordTypes(self):
-        raise NotImplementedError("Subclass must implement recordTypes()")
-
-
-    def listRecords(self, recordType):
-        raise NotImplementedError("Subclass must implement listRecords()")
-
-
-    def recordWithShortName(self, recordType, shortName):
-        for record in self.listRecords(recordType):
-            if shortName in record.shortNames:
-                return record
-        return None
-
-
-    def recordWithUID(self, uid):
-        uid = normalizeUUID(uid)
-        for record in self.allRecords():
-            if record.uid == uid:
-                return record
-        return None
-
-
-    def recordWithGUID(self, guid):
-        guid = normalizeUUID(guid)
-        for record in self.allRecords():
-            if record.guid == guid:
-                return record
-        return None
-
-
-    def recordWithAuthID(self, authID):
-        for record in self.allRecords():
-            if authID in record.authIDs:
-                return record
-        return None
-
-
-    def recordWithCalendarUserAddress(self, address):
-        address = normalizeCUAddr(address)
-        record = None
-        if address.startswith("urn:uuid:"):
-            guid = address[9:]
-            record = self.recordWithGUID(guid)
-        elif address.startswith("mailto:"):
-            for record in self.allRecords():
-                if address[7:] in record.emailAddresses:
-                    break
-            else:
-                return None
-        elif address.startswith("/principals/"):
-            parts = map(unquote, address.split("/"))
-            if len(parts) == 4:
-                if parts[2] == "__uids__":
-                    guid = parts[3]
-                    record = self.recordWithGUID(guid)
-                else:
-                    record = self.recordWithShortName(parts[2], parts[3])
-
-        return record if record and record.enabledForCalendaring else None
-
-
-    def recordWithCachedGroupsAlias(self, recordType, alias):
-        """
-        @param recordType: the type of the record to look up.
-        @param alias: the cached-groups alias of the record to look up.
-        @type alias: C{str}
-
-        @return: a deferred L{IDirectoryRecord} with the given cached-groups
-            alias, or C{None} if no such record is found.
-        """
-        # The default implementation uses guid
-        return succeed(self.recordWithGUID(alias))
-
-
-    def allRecords(self):
-        for recordType in self.recordTypes():
-            for record in self.listRecords(recordType):
-                yield record
-
-
-    def recordsMatchingFieldsWithCUType(self, fields, operand="or",
-        cuType=None):
-        if cuType:
-            recordType = DirectoryRecord.fromCUType(cuType)
-        else:
-            recordType = None
-
-        return self.recordsMatchingFields(fields, operand=operand,
-            recordType=recordType)
-
-
-    def recordTypesForSearchContext(self, context):
-        """
-        Map calendarserver-principal-search REPORT context value to applicable record types
-
-        @param context: The context value to map
-        @type context: C{str}
-        @returns: The list of record types the context maps to
-        @rtype: C{list} of C{str}
-        """
-        if context == self.searchContext_location:
-            recordTypes = [self.recordType_locations]
-        elif context == self.searchContext_resource:
-            recordTypes = [self.recordType_resources]
-        elif context == self.searchContext_user:
-            recordTypes = [self.recordType_users]
-        elif context == self.searchContext_group:
-            recordTypes = [self.recordType_groups]
-        elif context == self.searchContext_attendee:
-            recordTypes = [self.recordType_users, self.recordType_groups,
-                self.recordType_resources]
-        else:
-            recordTypes = list(self.recordTypes())
-        return recordTypes
-
-
-    def recordsMatchingTokens(self, tokens, context=None):
-        """
-        @param tokens: The tokens to search on
-        @type tokens: C{list} of C{str} (utf-8 bytes)
-        @param context: An indication of what the end user is searching
-            for; "attendee", "location", or None
-        @type context: C{str}
-        @return: a deferred sequence of L{IDirectoryRecord}s which
-            match the given tokens and optional context.
-
-        Each token is searched for within each record's full name and
-        email address; if each token is found within a record that
-        record is returned in the results.
-
-        If context is None, all record types are considered.  If
-        context is "location", only locations are considered.  If
-        context is "attendee", only users, groups, and resources
-        are considered.
-        """
-
-        # Default, bruteforce method; override with one optimized for each
-        # service
-
-        def fieldMatches(fieldValue, value):
-            if fieldValue is None:
-                return False
-            elif type(fieldValue) in types.StringTypes:
-                fieldValue = (fieldValue,)
-
-            for testValue in fieldValue:
-                testValue = testValue.lower()
-                value = value.lower()
-
-                try:
-                    testValue.index(value)
-                    return True
-                except ValueError:
-                    pass
-
-            return False
-
-        def recordMatches(record):
-            for token in tokens:
-                for fieldName in ["fullName", "emailAddresses"]:
-                    try:
-                        fieldValue = getattr(record, fieldName)
-                        if fieldMatches(fieldValue, token):
-                            break
-                    except AttributeError:
-                        # No value
-                        pass
-                else:
-                    return False
-            return True
-
-
-        def yieldMatches(recordTypes):
-            try:
-                for recordType in [r for r in recordTypes if r in self.recordTypes()]:
-                    for record in self.listRecords(recordType):
-                        if recordMatches(record):
-                            yield record
-
-            except UnknownRecordTypeError:
-                # Skip this service since it doesn't understand this record type
-                pass
-
-        recordTypes = self.recordTypesForSearchContext(context)
-        return succeed(yieldMatches(recordTypes))
-
-
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-        # Default, bruteforce method; override with one optimized for each
-        # service
-
-        def fieldMatches(fieldValue, value, caseless, matchType):
-            if fieldValue is None:
-                return False
-            elif type(fieldValue) in types.StringTypes:
-                fieldValue = (fieldValue,)
-
-            for testValue in fieldValue:
-                if caseless:
-                    testValue = testValue.lower()
-                    value = value.lower()
-
-                if matchType == 'starts-with':
-                    if testValue.startswith(value):
-                        return True
-                elif matchType == 'contains':
-                    try:
-                        testValue.index(value)
-                        return True
-                    except ValueError:
-                        pass
-                else: # exact
-                    if testValue == value:
-                        return True
-
-            return False
-
-        def recordMatches(record):
-            if operand == "and":
-                for fieldName, value, caseless, matchType in fields:
-                    try:
-                        fieldValue = getattr(record, fieldName)
-                        if not fieldMatches(fieldValue, value, caseless,
-                            matchType):
-                            return False
-                    except AttributeError:
-                        # No property => no match
-                        return False
-                # we hit on every property
-                return True
-            else: # "or"
-                for fieldName, value, caseless, matchType in fields:
-                    try:
-                        fieldValue = getattr(record, fieldName)
-                        if fieldMatches(fieldValue, value, caseless,
-                            matchType):
-                            return True
-                    except AttributeError:
-                        # No value
-                        pass
-                # we didn't hit any
-                return False
-
-        def yieldMatches(recordType):
-            try:
-                if recordType is None:
-                    recordTypes = list(self.recordTypes())
-                else:
-                    recordTypes = (recordType,)
-
-                for recordType in recordTypes:
-                    for record in self.listRecords(recordType):
-                        if recordMatches(record):
-                            yield record
-
-            except UnknownRecordTypeError:
-                # Skip this service since it doesn't understand this record type
-                pass
-
-        return succeed(yieldMatches(recordType))
-
-
-    def getGroups(self, guids):
-        """
-        This implementation returns all groups, not just the ones specified
-        by guids
-        """
-        return succeed(self.listRecords(self.recordType_groups))
-
-
-    def getResourceInfo(self):
-        return ()
-
-
-    def isAvailable(self):
-        return True
-
-
-    def getParams(self, params, defaults, ignore=None):
-        """ Checks configuration parameters for unexpected/ignored keys, and
-            applies default values. """
-
-        keys = set(params.keys())
-
-        result = {}
-        for key in defaults.iterkeys():
-            if key in params:
-                result[key] = params[key]
-                keys.remove(key)
-            else:
-                result[key] = defaults[key]
-
-        if ignore:
-            for key in ignore:
-                if key in params:
-                    self.log.warn("Ignoring obsolete directory service parameter: %s" % (key,))
-                    keys.remove(key)
-
-        if keys:
-            raise DirectoryConfigurationError("Invalid directory service parameter(s): %s" % (", ".join(list(keys)),))
-        return result
-
-
-    def parseResourceInfo(self, plist, guid, recordType, shortname):
-        """
-        Parse ResourceInfo plist 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)
-            autoAcceptGroup = wpframework.get("AutoAcceptGroup", "")
-        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, autoAcceptGroup)
-
-
-    def getExternalProxyAssignments(self):
-        """
-        Retrieve proxy assignments for locations and resources from the
-        directory and return a list of (principalUID, ([memberUIDs)) tuples,
-        suitable for passing to proxyDB.setGroupMembers( )
-
-        This generic implementation fetches all locations and resources.
-        More specialized implementations can perform whatever operation is
-        most efficient for their particular directory service.
-        """
-        assignments = []
-
-        resources = itertools.chain(
-            self.listRecords(self.recordType_locations),
-            self.listRecords(self.recordType_resources)
-        )
-        for record in resources:
-            guid = record.guid
-            if record.enabledForCalendaring:
-                assignments.append(("%s#calendar-proxy-write" % (guid,),
-                                   record.externalProxies()))
-                assignments.append(("%s#calendar-proxy-read" % (guid,),
-                                   record.externalReadOnlyProxies()))
-
-        return assignments
-
-
-    def createRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, password=None, **kwargs):
-        """
-        Create/persist a directory record based on the given values
-        """
-        raise NotImplementedError("Subclass must implement createRecord")
-
-
-    def updateRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, password=None, **kwargs):
-        """
-        Update/persist a directory record based on the given values
-        """
-        raise NotImplementedError("Subclass must implement updateRecord")
-
-
-    def destroyRecord(self, recordType, guid=None):
-        """
-        Remove a directory record from the directory
-        """
-        raise NotImplementedError("Subclass must implement destroyRecord")
-
-
-    def createRecords(self, data):
-        """
-        Create directory records in bulk
-        """
-        raise NotImplementedError("Subclass must implement createRecords")
-
-
-    def setPrincipalCollection(self, principalCollection):
-        """
-        Set the principal service that the directory relies on for doing proxy tests.
-
-        @param principalService: the principal service.
-        @type principalService: L{DirectoryProvisioningResource}
-        """
-        self.principalCollection = principalCollection
-
-
-    def isProxyFor(self, test, other):
-        """
-        Test whether one record is a calendar user proxy for the specified record.
-
-        @param test: record to test
-        @type test: L{DirectoryRecord}
-        @param other: record to check against
-        @type other: L{DirectoryRecord}
-
-        @return: C{True} if test is a proxy of other.
-        @rtype: C{bool}
-        """
-        return self.principalCollection.isProxyFor(test, other)
-
-
-
-class GroupMembershipCache(Memcacher):
-    """
-    Caches group membership information
-
-    This cache is periodically updated by a side car so that worker processes
-    never have to ask the directory service directly for group membership
-    information.
-
-    Keys in this cache are:
-
-    "groups-for:<GUID>" : comma-separated list of groups that GUID is a member
-    of.  Note that when using LDAP, the key for this is an LDAP DN.
-
-    "group-cacher-populated" : contains a datestamp indicating the most recent
-    population.
-    """
-    log = Logger()
-
-    def __init__(self, namespace, pickle=True, no_invalidation=False,
-        key_normalization=True, expireSeconds=0, lockSeconds=60):
-
-        super(GroupMembershipCache, self).__init__(namespace, pickle=pickle,
-            no_invalidation=no_invalidation,
-            key_normalization=key_normalization)
-
-        self.expireSeconds = expireSeconds
-        self.lockSeconds = lockSeconds
-
-
-    def setGroupsFor(self, guid, memberships):
-        self.log.debug("set groups-for %s : %s" % (guid, memberships))
-        return self.set("groups-for:%s" %
-            (str(guid)), memberships,
-            expireTime=self.expireSeconds)
-
-
-    def getGroupsFor(self, guid):
-        self.log.debug("get groups-for %s" % (guid,))
-        def _value(value):
-            if value:
-                return value
-            else:
-                return set()
-        d = self.get("groups-for:%s" % (str(guid),))
-        d.addCallback(_value)
-        return d
-
-
-    def deleteGroupsFor(self, guid):
-        self.log.debug("delete groups-for %s" % (guid,))
-        return self.delete("groups-for:%s" % (str(guid),))
-
-
-    def setPopulatedMarker(self):
-        self.log.debug("set group-cacher-populated")
-        return self.set("group-cacher-populated", str(datetime.datetime.now()))
-
-
-    @inlineCallbacks
-    def isPopulated(self):
-        self.log.debug("is group-cacher-populated")
-        value = (yield self.get("group-cacher-populated"))
-        returnValue(value is not None)
-
-
-    def acquireLock(self):
-        """
-        Acquire a memcached lock named group-cacher-lock
-
-        return: Deferred firing True if successful, False if someone already has
-            the lock
-        """
-        self.log.debug("add group-cacher-lock")
-        return self.add("group-cacher-lock", "1", expireTime=self.lockSeconds)
-
-
-    def extendLock(self):
-        """
-        Update the expiration time of the memcached lock
-        Return: Deferred firing True if successful, False otherwise
-        """
-        self.log.debug("extend group-cacher-lock")
-        return self.set("group-cacher-lock", "1", expireTime=self.lockSeconds)
-
-
-    def releaseLock(self):
-        """
-        Release the memcached lock
-        Return: Deferred firing True if successful, False otherwise
-        """
-        self.log.debug("delete group-cacher-lock")
-        return self.delete("group-cacher-lock")
-
-
-
-class GroupMembershipCacheUpdater(object):
-    """
-    Responsible for updating memcached with group memberships.  This will run
-    in a sidecar.  There are two sources of proxy data to pull from: the local
-    proxy database, and the location/resource info in the directory system.
-    """
-    log = Logger()
-
-    def __init__(self, proxyDB, directory, updateSeconds, expireSeconds,
-        lockSeconds, cache=None, namespace=None, useExternalProxies=False,
-        externalProxiesSource=None):
-        self.proxyDB = proxyDB
-        self.directory = directory
-        self.updateSeconds = updateSeconds
-        self.useExternalProxies = useExternalProxies
-        if useExternalProxies and externalProxiesSource is None:
-            externalProxiesSource = self.directory.getExternalProxyAssignments
-        self.externalProxiesSource = externalProxiesSource
-
-        if cache is None:
-            assert namespace is not None, "namespace must be specified if GroupMembershipCache is not provided"
-            cache = GroupMembershipCache(namespace, expireSeconds=expireSeconds,
-                lockSeconds=lockSeconds)
-        self.cache = cache
-
-
-    @inlineCallbacks
-    def getGroups(self, guids=None):
-        """
-        Retrieve all groups and their member info (but don't actually fault in
-        the records of the members), and return two dictionaries.  The first
-        contains group records; the keys for this dictionary are the identifiers
-        used by the directory service to specify members.  In OpenDirectory
-        these would be guids, but in LDAP these could be DNs, or some other
-        attribute.  This attribute can be retrieved from a record using
-        record.cachedGroupsAlias().
-        The second dictionary returned maps that member attribute back to the
-        corresponding guid.  These dictionaries are used to reverse-index the
-        groups that users are in by expandedMembers().
-
-        @param guids: if provided, retrieve only the groups corresponding to
-            these guids (including their sub groups)
-        @type guids: list of guid strings
-        """
-        groups = {}
-        aliases = {}
-
-        if guids is None: # get all group guids
-            records = self.directory.listRecords(self.directory.recordType_groups)
-        else: # get only the ones we know have been delegated to
-            records = (yield self.directory.getGroups(guids))
-
-        for record in records:
-            alias = record.cachedGroupsAlias()
-            groups[alias] = record.memberGUIDs()
-            aliases[record.guid] = alias
-
-        returnValue((groups, aliases))
-
-
-    def expandedMembers(self, groups, guid, members=None, seen=None):
-        """
-        Return the complete, flattened set of members of a group, including
-        all sub-groups, based on the group hierarchy described in the
-        groups dictionary.
-        """
-        if members is None:
-            members = set()
-        if seen is None:
-            seen = set()
-
-        if guid not in seen:
-            seen.add(guid)
-            for member in groups[guid]:
-                members.add(member)
-                if member in groups: # it's a group then
-                    self.expandedMembers(groups, member, members=members,
-                                         seen=seen)
-        return members
-
-
-    @inlineCallbacks
-    def updateCache(self, fast=False):
-        """
-        Iterate the proxy database to retrieve all the principals who have been
-        delegated to.  Fault these principals in.  For any of these principals
-        that are groups, expand the members of that group and store those in
-        the cache
-
-        If fast=True, we're in quick-start mode, used only by the master process
-        to start servicing requests as soon as possible.  In this mode we look
-        for DataRoot/memberships_cache which is a pickle of a dictionary whose
-        keys are guids (except when using LDAP where the keys will be DNs), and
-        the values are lists of group guids.  If the cache file does not exist
-        we switch to fast=False.
-
-        The return value is mainly used for unit tests; it's a tuple containing
-        the (possibly modified) value for fast, and the number of members loaded
-        into the cache (which can be zero if fast=True and isPopulated(), or
-        fast=False and the cache is locked by someone else).
-
-        The pickled snapshot file is a dict whose keys represent a record and
-        the values are the guids of the groups that record is a member of.  The
-        keys are normally guids except in the case of a directory system like LDAP
-        where there can be a different attribute used for referring to members,
-        such as a DN.
-        """
-
-        # TODO: add memcached eviction protection
-
-        useLock = True
-
-        # See if anyone has completely populated the group membership cache
-        isPopulated = (yield self.cache.isPopulated())
-
-        if fast:
-            # We're in quick-start mode.  Check first to see if someone has
-            # populated the membership cache, and if so, return immediately
-            if isPopulated:
-                self.log.info("Group membership cache is already populated")
-                returnValue((fast, 0, 0))
-
-            # We don't care what others are doing right now, we need to update
-            useLock = False
-
-        self.log.info("Updating group membership cache")
-
-        dataRoot = FilePath(config.DataRoot)
-        membershipsCacheFile = dataRoot.child("memberships_cache")
-        extProxyCacheFile = dataRoot.child("external_proxy_cache")
-
-        if not membershipsCacheFile.exists():
-            self.log.info("Group membership snapshot file does not yet exist")
-            fast = False
-            previousMembers = {}
-            callGroupsChanged = False
-        else:
-            self.log.info("Group membership snapshot file exists: %s" %
-                (membershipsCacheFile.path,))
-            callGroupsChanged = True
-            try:
-                previousMembers = pickle.loads(membershipsCacheFile.getContent())
-            except:
-                self.log.warn("Could not parse snapshot; will regenerate cache")
-                fast = False
-                previousMembers = {}
-                callGroupsChanged = False
-
-        if useLock:
-            self.log.info("Attempting to acquire group membership cache lock")
-            acquiredLock = (yield self.cache.acquireLock())
-            if not acquiredLock:
-                self.log.info("Group membership cache lock held by another process")
-                returnValue((fast, 0, 0))
-            self.log.info("Acquired lock")
-
-        if not fast and self.useExternalProxies:
-
-            # Load in cached copy of external proxies so we can diff against them
-            previousAssignments = []
-            if extProxyCacheFile.exists():
-                self.log.info("External proxies snapshot file exists: %s" %
-                    (extProxyCacheFile.path,))
-                try:
-                    previousAssignments = pickle.loads(extProxyCacheFile.getContent())
-                except:
-                    self.log.warn("Could not parse external proxies snapshot")
-                    previousAssignments = []
-
-            if useLock:
-                yield self.cache.extendLock()
-
-            self.log.info("Retrieving proxy assignments from directory")
-            assignments = self.externalProxiesSource()
-            self.log.info("%d proxy assignments retrieved from directory" %
-                (len(assignments),))
-
-            if useLock:
-                yield self.cache.extendLock()
-
-            changed, removed = diffAssignments(previousAssignments, assignments)
-            # changed is the list of proxy assignments (either new or updates).
-            # removed is the list of principals who used to have an external
-            #   delegate but don't anymore.
-
-            # populate proxy DB from external resource info
-            if changed:
-                self.log.info("Updating proxy assignments")
-                assignmentCount = 0
-                totalNumAssignments = len(changed)
-                currentAssignmentNum = 0
-                for principalUID, members in changed:
-                    currentAssignmentNum += 1
-                    if currentAssignmentNum % 1000 == 0:
-                        self.log.info("...proxy assignment %d of %d" % (currentAssignmentNum,
-                            totalNumAssignments))
-                    try:
-                        current = (yield self.proxyDB.getMembers(principalUID))
-                        if members != current:
-                            assignmentCount += 1
-                            yield self.proxyDB.setGroupMembers(principalUID, members)
-                    except Exception, e:
-                        self.log.error("Unable to update proxy assignment: principal=%s, members=%s, error=%s" % (principalUID, members, e))
-                self.log.info("Updated %d assignment%s in proxy database" %
-                    (assignmentCount, "" if assignmentCount == 1 else "s"))
-
-            if removed:
-                self.log.info("Deleting proxy assignments")
-                assignmentCount = 0
-                totalNumAssignments = len(removed)
-                currentAssignmentNum = 0
-                for principalUID in removed:
-                    currentAssignmentNum += 1
-                    if currentAssignmentNum % 1000 == 0:
-                        self.log.info("...proxy assignment %d of %d" % (currentAssignmentNum,
-                            totalNumAssignments))
-                    try:
-                        assignmentCount += 1
-                        yield self.proxyDB.setGroupMembers(principalUID, [])
-                    except Exception, e:
-                        self.log.error("Unable to remove proxy assignment: principal=%s, members=%s, error=%s" % (principalUID, members, e))
-                self.log.info("Removed %d assignment%s from proxy database" %
-                    (assignmentCount, "" if assignmentCount == 1 else "s"))
-
-            # Store external proxy snapshot
-            self.log.info("Taking snapshot of external proxies to %s" %
-                (extProxyCacheFile.path,))
-            extProxyCacheFile.setContent(pickle.dumps(assignments))
-
-        if fast:
-            # If there is an on-disk snapshot of the membership information,
-            # load that and put into memcached, bypassing the faulting in of
-            # any records, so that the server can start up quickly.
-
-            self.log.info("Loading group memberships from snapshot")
-            members = pickle.loads(membershipsCacheFile.getContent())
-
-        else:
-            # Fetch the group hierarchy from the directory, fetch the list
-            # of delegated-to guids, intersect those and build a dictionary
-            # containing which delegated-to groups a user is a member of
-
-            self.log.info("Retrieving list of all proxies")
-            # This is always a set of guids:
-            delegatedGUIDs = set((yield self.proxyDB.getAllMembers()))
-            self.log.info("There are %d proxies" % (len(delegatedGUIDs),))
-            self.log.info("Retrieving group hierarchy from directory")
-
-            # "groups" maps a group to its members; the keys and values consist
-            # of whatever directory attribute is used to refer to members.  The
-            # attribute value comes from record.cachedGroupsAlias().
-            # "aliases" maps the record.cachedGroupsAlias() value for a group
-            # back to the group's guid.
-            groups, aliases = (yield self.getGroups(guids=delegatedGUIDs))
-            groupGUIDs = set(aliases.keys())
-            self.log.info("%d groups retrieved from the directory" %
-                (len(groupGUIDs),))
-
-            delegatedGUIDs = delegatedGUIDs.intersection(groupGUIDs)
-            self.log.info("%d groups are proxies" % (len(delegatedGUIDs),))
-
-            # Reverse index the group membership from cache
-            members = {}
-            for groupGUID in delegatedGUIDs:
-                groupMembers = self.expandedMembers(groups, aliases[groupGUID])
-                # groupMembers is in cachedGroupsAlias() format
-                for member in groupMembers:
-                    memberships = members.setdefault(member, set())
-                    memberships.add(groupGUID)
-
-            self.log.info("There are %d users delegated-to via groups" %
-                (len(members),))
-
-            # Store snapshot
-            self.log.info("Taking snapshot of group memberships to %s" %
-                (membershipsCacheFile.path,))
-            membershipsCacheFile.setContent(pickle.dumps(members))
-
-            # Update ownership
-            uid = gid = -1
-            if config.UserName:
-                uid = pwd.getpwnam(config.UserName).pw_uid
-            if config.GroupName:
-                gid = grp.getgrnam(config.GroupName).gr_gid
-            os.chown(membershipsCacheFile.path, uid, gid)
-            if extProxyCacheFile.exists():
-                os.chown(extProxyCacheFile.path, uid, gid)
-
-        self.log.info("Storing %d group memberships in memcached" %
-                       (len(members),))
-        changedMembers = set()
-        totalNumMembers = len(members)
-        currentMemberNum = 0
-        for member, groups in members.iteritems():
-            currentMemberNum += 1
-            if currentMemberNum % 1000 == 0:
-                self.log.info("...membership %d of %d" % (currentMemberNum,
-                    totalNumMembers))
-            # self.log.debug("%s is in %s" % (member, groups))
-            yield self.cache.setGroupsFor(member, groups)
-            if groups != previousMembers.get(member, None):
-                # This principal has had a change in group membership
-                # so invalidate the PROPFIND response cache
-                changedMembers.add(member)
-            try:
-                # Remove from previousMembers; anything still left in
-                # previousMembers when this loop is done will be
-                # deleted from cache (since only members that were
-                # previously in delegated-to groups but are no longer
-                # would still be in previousMembers)
-                del previousMembers[member]
-            except KeyError:
-                pass
-
-        # Remove entries for principals that no longer are in delegated-to
-        # groups
-        for member, groups in previousMembers.iteritems():
-            yield self.cache.deleteGroupsFor(member)
-            changedMembers.add(member)
-
-        # For principals whose group membership has changed, call groupsChanged()
-        if callGroupsChanged and not fast and hasattr(self.directory, "principalCollection"):
-            for member in changedMembers:
-                record = yield self.directory.recordWithCachedGroupsAlias(
-                    self.directory.recordType_users, member)
-                if record is not None:
-                    principal = self.directory.principalCollection.principalForRecord(record)
-                    if principal is not None:
-                        self.log.debug("Group membership changed for %s (%s)" %
-                            (record.shortNames[0], record.guid,))
-                        if hasattr(principal, "groupsChanged"):
-                            yield principal.groupsChanged()
-
-        yield self.cache.setPopulatedMarker()
-
-        if useLock:
-            self.log.info("Releasing lock")
-            yield self.cache.releaseLock()
-
-        self.log.info("Group memberships cache updated")
-
-        returnValue((fast, len(members), len(changedMembers)))
-
-
-
-def diffAssignments(old, new):
-    """
-    Compare two proxy assignment lists and return their differences in the form of
-    two lists -- one for added/updated assignments, and one for removed assignments.
-    @param old: list of (group, set(members)) tuples
-    @type old: C{list}
-    @param new: list of (group, set(members)) tuples
-    @type new: C{list}
-    @return: Tuple of two lists; the first list contains tuples of (proxy-principal,
-        set(members)), and represents all the new or updated assignments.  The
-        second list contains all the proxy-principals which used to have a delegate
-        but don't anymore.
-    """
-    old = dict(old)
-    new = dict(new)
-    changed = []
-    removed = []
-    for key in old.iterkeys():
-        if key not in new:
-            removed.append(key)
-        else:
-            if old[key] != new[key]:
-                changed.append((key, new[key]))
-    for key in new.iterkeys():
-        if key not in old:
-            changed.append((key, new[key]))
-    return changed, removed
-
-
-
-class DirectoryRecord(object):
-    log = Logger()
-
-    implements(IDirectoryRecord, ICalendarStoreDirectoryRecord)
-
-    def __repr__(self):
-        return "<%s[%s@%s(%s)] %s(%s) %r @ %s>" % (
-            self.__class__.__name__,
-            self.recordType,
-            self.service.guid,
-            self.service.realmName,
-            self.guid,
-            ",".join(self.shortNames),
-            self.fullName,
-            self.serverURI(),
-        )
-
-
-    def __init__(
-        self, service, recordType, guid=None,
-        shortNames=(), authIDs=set(), fullName=None,
-        firstName=None, lastName=None, emailAddresses=set(),
-        calendarUserAddresses=set(),
-        autoSchedule=False, autoScheduleMode=None,
-        autoAcceptGroup="",
-        enabledForCalendaring=None,
-        enabledForAddressBooks=None,
-        uid=None,
-        enabledForLogin=True,
-        extProxies=(), extReadOnlyProxies=(),
-        **kwargs
-    ):
-        assert service.realmName is not None
-        assert recordType
-        assert shortNames and isinstance(shortNames, tuple)
-
-        guid = normalizeUUID(guid)
-
-        if uid is None:
-            uid = guid
-
-        if fullName is None:
-            fullName = ""
-
-        self.service = service
-        self.recordType = recordType
-        self.guid = guid
-        self.uid = uid
-        self.enabled = False
-        self.serverID = ""
-        self.shortNames = shortNames
-        self.authIDs = authIDs
-        self.fullName = fullName
-        self.firstName = firstName
-        self.lastName = lastName
-        self.emailAddresses = emailAddresses
-        self.enabledForCalendaring = enabledForCalendaring
-        self.autoSchedule = autoSchedule
-        self.autoScheduleMode = autoScheduleMode
-        self.autoAcceptGroup = autoAcceptGroup
-        self.enabledForAddressBooks = enabledForAddressBooks
-        self.enabledForLogin = enabledForLogin
-        self.extProxies = extProxies
-        self.extReadOnlyProxies = extReadOnlyProxies
-        self.extras = kwargs
-
-
-    def get_calendarUserAddresses(self):
-        """
-        Dynamically construct a calendarUserAddresses attribute which describes
-        this L{DirectoryRecord}.
-
-        @see: L{IDirectoryRecord.calendarUserAddresses}.
-        """
-        if not self.enabledForCalendaring:
-            return frozenset()
-        cuas = set(
-            ["mailto:%s" % (emailAddress,)
-             for emailAddress in self.emailAddresses]
-        )
-        if self.guid:
-            cuas.add("urn:uuid:%s" % (self.guid,))
-            cuas.add(joinURL("/principals", "__uids__", self.guid) + "/")
-        for shortName in self.shortNames:
-            cuas.add(joinURL("/principals", self.recordType, shortName,) + "/")
-
-        return frozenset(cuas)
-
-    calendarUserAddresses = property(get_calendarUserAddresses)
-
-    def __cmp__(self, other):
-        if not isinstance(other, DirectoryRecord):
-            return NotImplemented
-
-        for attr in ("service", "recordType", "shortNames", "guid"):
-            diff = cmp(getattr(self, attr), getattr(other, attr))
-            if diff != 0:
-                return diff
-        return 0
-
-
-    def __hash__(self):
-        h = hash(self.__class__.__name__)
-        for attr in ("service", "recordType", "shortNames", "guid",
-                     "enabled", "enabledForCalendaring"):
-            h = (h + hash(getattr(self, attr))) & sys.maxint
-
-        return h
-
-
-    def cacheToken(self):
-        """
-        Generate a token that can be uniquely used to identify the state of this record for use
-        in a cache.
-        """
-        return hash((
-            self.__class__.__name__,
-            self.service.realmName,
-            self.recordType,
-            self.shortNames,
-            self.guid,
-            self.enabled,
-            self.enabledForCalendaring,
-        ))
-
-
-    def addAugmentInformation(self, augment):
-
-        if augment:
-            self.enabled = augment.enabled
-            self.serverID = augment.serverID
-            self.enabledForCalendaring = augment.enabledForCalendaring
-            self.enabledForAddressBooks = augment.enabledForAddressBooks
-            self.autoSchedule = augment.autoSchedule
-            self.autoScheduleMode = augment.autoScheduleMode
-            self.autoAcceptGroup = augment.autoAcceptGroup
-            self.enabledForLogin = augment.enabledForLogin
-
-            if (self.enabledForCalendaring or self.enabledForAddressBooks) and self.recordType == self.service.recordType_groups:
-                self.enabledForCalendaring = False
-                self.enabledForAddressBooks = False
-
-                # For augment records cloned from the Default augment record,
-                # don't emit this message:
-                if not augment.clonedFromDefault:
-                    self.log.error("Group '%s(%s)' cannot be enabled for calendaring or address books" % (self.guid, self.shortNames[0],))
-
-        else:
-            # Groups are by default always enabled
-            self.enabled = (self.recordType == self.service.recordType_groups)
-            self.serverID = ""
-            self.enabledForCalendaring = False
-            self.enabledForAddressBooks = False
-            self.enabledForLogin = False
-
-
-    def applySACLs(self):
-        """
-        Disable calendaring and addressbooks as dictated by SACLs
-        """
-
-        if config.EnableSACLs and self.CheckSACL:
-            username = self.shortNames[0]
-            if self.CheckSACL(username, "calendar") != 0:
-                self.log.debug("%s is not enabled for calendaring due to SACL"
-                               % (username,))
-                self.enabledForCalendaring = False
-            if self.CheckSACL(username, "addressbook") != 0:
-                self.log.debug("%s is not enabled for addressbooks due to SACL"
-                               % (username,))
-                self.enabledForAddressBooks = False
-
-
-    def displayName(self):
-        return self.fullName if self.fullName else self.shortNames[0]
-
-
-    def isLoginEnabled(self):
-        """
-        Returns True if the user should be allowed to log in, based on the
-        enabledForLogin attribute, which is currently controlled by the
-        DirectoryService implementation.
-        """
-        return self.enabledForLogin
-
-
-    def members(self):
-        return ()
-
-
-    def expandedMembers(self, members=None, seen=None):
-        """
-        Return the complete, flattened set of members of a group, including
-        all sub-groups.
-        """
-        if members is None:
-            members = set()
-        if seen is None:
-            seen = set()
-
-        if self not in seen:
-            seen.add(self)
-            for member in self.members():
-                members.add(member)
-                if member.recordType == self.service.recordType_groups:
-                    member.expandedMembers(members=members, seen=seen)
-
-        return members
-
-
-    def groups(self):
-        return ()
-
-
-    def cachedGroups(self):
-        """
-        Return the set of groups (guids) this record is a member of, based on
-        the data cached by cacheGroupMembership( )
-        """
-        return self.service.groupMembershipCache.getGroupsFor(self.cachedGroupsAlias())
-
-
-    def cachedGroupsAlias(self):
-        """
-        The GroupMembershipCache uses keys based on this value.  Normally it's
-        a record's guid but in a directory system like LDAP which can use a
-        different attribute to refer to group members, we need to be able to
-        look up an entry in the GroupMembershipCache by that attribute.
-        Subclasses which don't use record.guid to look up group membership
-        should override this method.
-        """
-        return self.guid
-
-
-    def externalProxies(self):
-        """
-        Return the set of proxies defined in the directory service, as opposed
-        to assignments in the proxy DB itself.
-        """
-        return set(self.extProxies)
-
-
-    def externalReadOnlyProxies(self):
-        """
-        Return the set of read-only proxies defined in the directory service,
-        as opposed to assignments in the proxy DB itself.
-        """
-        return set(self.extReadOnlyProxies)
-
-
-    def memberGUIDs(self):
-        """
-        Return the set of GUIDs that are members of this group
-        """
-        return set()
-
-
-    def verifyCredentials(self, credentials):
-        return False
-
-
-    def calendarsEnabled(self):
-        return config.EnableCalDAV and self.enabledForCalendaring
-
-
-    def canonicalCalendarUserAddress(self):
-        """
-            Return a CUA for this principal, preferring in this order:
-            urn:uuid: form
-            mailto: form
-            first in calendarUserAddresses list
-        """
-
-        cua = ""
-        for candidate in self.calendarUserAddresses:
-            # Pick the first one, but urn:uuid: and mailto: can override
-            if not cua:
-                cua = candidate
-            # But always immediately choose the urn:uuid: form
-            if candidate.startswith("urn:uuid:"):
-                cua = candidate
-                break
-            # Prefer mailto: if no urn:uuid:
-            elif candidate.startswith("mailto:"):
-                cua = candidate
-        return cua
-
-
-    def enabledAsOrganizer(self):
-        if self.recordType == DirectoryService.recordType_users:
-            return True
-        elif self.recordType == DirectoryService.recordType_groups:
-            return config.Scheduling.Options.AllowGroupAsOrganizer
-        elif self.recordType == DirectoryService.recordType_locations:
-            return config.Scheduling.Options.AllowLocationAsOrganizer
-        elif self.recordType == DirectoryService.recordType_resources:
-            return config.Scheduling.Options.AllowResourceAsOrganizer
-        else:
-            return False
-
-    # Mapping from directory record.recordType to RFC2445 CUTYPE values
-    _cuTypes = {
-        'users' : 'INDIVIDUAL',
-        'groups' : 'GROUP',
-        'resources' : 'RESOURCE',
-        'locations' : 'ROOM',
-    }
-
-    def getCUType(self):
-        return self._cuTypes.get(self.recordType, "UNKNOWN")
-
-
-    @classmethod
-    def fromCUType(cls, cuType):
-        for key, val in cls._cuTypes.iteritems():
-            if val == cuType:
-                return key
-        return None
-
-
-    def canAutoSchedule(self, organizer):
-        if config.Scheduling.Options.AutoSchedule.Enabled:
-            if (config.Scheduling.Options.AutoSchedule.Always or
-                self.autoSchedule or
-                self.autoAcceptFromOrganizer(organizer)):
-                if (self.getCUType() != "INDIVIDUAL" or
-                    config.Scheduling.Options.AutoSchedule.AllowUsers):
-                    return True
-        return False
-
-
-    def getAutoScheduleMode(self, organizer):
-        autoScheduleMode = self.autoScheduleMode
-        if self.autoAcceptFromOrganizer(organizer):
-            autoScheduleMode = "automatic"
-        return autoScheduleMode
-
-
-    def autoAcceptFromOrganizer(self, organizer):
-        if organizer is not None and self.autoAcceptGroup is not None:
-            service = self.service.aggregateService or self.service
-            organizerRecord = service.recordWithCalendarUserAddress(organizer)
-            if organizerRecord is not None:
-                if organizerRecord.guid in self.autoAcceptMembers():
-                    return True
-        return False
-
-
-    def serverURI(self):
-        """
-        URL of the server hosting this record. Return None if hosted on this server.
-        """
-        if config.Servers.Enabled and self.serverID:
-            return Servers.getServerURIById(self.serverID)
-        else:
-            return None
-
-
-    def server(self):
-        """
-        Server hosting this record. Return None if hosted on this server.
-        """
-        if config.Servers.Enabled and self.serverID:
-            return Servers.getServerById(self.serverID)
-        else:
-            return None
-
-
-    def thisServer(self):
-        s = self.server()
-        return s.thisServer if s is not None else True
-
-
-    def autoAcceptMembers(self):
-        """
-        Return the list of GUIDs for which this record will automatically accept
-        invites from (assuming no conflicts).  This list is based on the group
-        assigned to record.autoAcceptGroup.  Cache the expanded group membership
-        within the record.
-
-        @return: the list of members of the autoAcceptGroup, or an empty list if
-            not assigned
-        @rtype: C{list} of GUID C{str}
-        """
-        if not hasattr(self, "_cachedAutoAcceptMembers"):
-            self._cachedAutoAcceptMembers = []
-            if self.autoAcceptGroup:
-                service = self.service.aggregateService or self.service
-                groupRecord = service.recordWithGUID(self.autoAcceptGroup)
-                if groupRecord is not None:
-                    self._cachedAutoAcceptMembers = [m.guid for m in groupRecord.expandedMembers()]
-
-        return self._cachedAutoAcceptMembers
-
-
-    def isProxyFor(self, other):
-        """
-        Test whether the record is a calendar user proxy for the specified record.
-
-        @param other: record to test
-        @type other: L{DirectoryRecord}
-
-        @return: C{True} if it is a proxy.
-        @rtype: C{bool}
-        """
-        return self.service.isProxyFor(self, other)
-
-
-
-class DirectoryError(RuntimeError):
-    """
-    Generic directory error.
-    """
-
-
-
-class DirectoryConfigurationError(DirectoryError):
-    """
-    Invalid directory configuration.
-    """
-
-
-
-class UnknownRecordTypeError(DirectoryError):
-    """
-    Unknown directory record type.
-    """
-    def __init__(self, recordType):
-        DirectoryError.__init__(self, "Invalid record type: %s" % (recordType,))
-        self.recordType = recordType
-
-
-# So CheckSACL will be parameterized
-# We do this after DirectoryRecord is defined
-try:
-    from calendarserver.platform.darwin._sacl import CheckSACL
-    DirectoryRecord.CheckSACL = CheckSACL
-except ImportError:
-    DirectoryRecord.CheckSACL = None

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/idirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/idirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/idirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,180 +0,0 @@
-##
-# Copyright (c) 2006-2014 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.
-##
-
-"""
-Directory service interfaces.
-"""
-
-__all__ = [
-    "IDirectoryService",
-    "IDirectoryRecord",
-]
-
-from zope.interface import Attribute, Interface
-
-class IDirectoryService(Interface):
-    """
-    Directory Service
-    """
-    realmName = Attribute("The name of the authentication realm this service represents.")
-    guid = Attribute("A GUID for this service.")
-
-    def recordTypes(): #@NoSelf
-        """
-        @return: a sequence of strings denoting the record types that
-            are kept in the directory.  For example: C{["users",
-            "groups", "resources"]}.
-        """
-
-    def listRecords(recordType): #@NoSelf
-        """
-        @param type: the type of records to retrieve.
-        @return: an iterable of records of the given type.
-        """
-
-    def recordWithShortName(recordType, shortName): #@NoSelf
-        """
-        @param recordType: the type of the record to look up.
-        @param shortName: the short name of the record to look up.
-        @return: an L{IDirectoryRecord} with the given short name, or
-            C{None} if no such record exists.
-        """
-
-    def recordWithUID(uid): #@NoSelf
-        """
-        @param uid: the UID of the record to look up.
-        @return: an L{IDirectoryRecord} with the given UID, or C{None}
-            if no such record exists.
-        """
-
-    def recordWithGUID(guid): #@NoSelf
-        """
-        @param guid: the GUID of the record to look up.
-        @return: an L{IDirectoryRecord} with the given GUID, or
-            C{None} if no such record exists.
-        """
-
-    def recordWithCalendarUserAddress(address): #@NoSelf
-        """
-        @param address: the calendar user address of the record to look up.
-        @type address: C{str}
-
-        @return: an L{IDirectoryRecord} with the given calendar user
-            address, or C{None} if no such record is found.  Note that
-            some directory services may not be able to locate records
-            by calendar user address, or may return partial results.
-            Note also that the calendar server may add to the list of
-            valid calendar user addresses for a user, and the
-            directory service may not be aware of these addresses.
-        """
-
-    def recordWithCachedGroupsAlias(recordType, alias): #@NoSelf
-        """
-        @param recordType: the type of the record to look up.
-        @param alias: the cached-groups alias of the record to look up.
-        @type alias: C{str}
-
-        @return: a deferred L{IDirectoryRecord} with the given cached-groups
-            alias, or C{None} if no such record is found.
-        """
-
-    def recordsMatchingFields(fields): #@NoSelf
-        """
-        @return: a deferred sequence of L{IDirectoryRecord}s which
-            match the given fields.
-        """
-
-    def recordsMatchingTokens(tokens, context=None): #@NoSelf
-        """
-        @param tokens: The tokens to search on
-        @type tokens: C{list} of C{str} (utf-8 bytes)
-
-        @param context: An indication of what the end user is searching for;
-            "attendee", "location", or None
-        @type context: C{str}
-
-        @return: a deferred sequence of L{IDirectoryRecord}s which match the
-            given tokens and optional context.
-
-            Each token is searched for within each record's full name and email
-            address; if each token is found within a record that record is
-            returned in the results.
-
-            If context is None, all record types are considered.  If context is
-            "location", only locations are considered.  If context is
-            "attendee", only users, groups, and resources are considered.
-        """
-
-    def setRealm(realmName): #@NoSelf
-        """
-        Set a new realm name for this (and nested services if any)
-
-        @param realmName: the realm name this service should use.
-        """
-
-
-
-class IDirectoryRecord(Interface):
-    """
-    Directory Record
-    """
-    service = Attribute("The L{IDirectoryService} this record exists in.")
-    recordType = Attribute("The type of this record.")
-    guid = Attribute("The GUID of this record.")
-    uid = Attribute("The UID of this record.")
-    enabled = Attribute("Determines whether this record should allow a principal to be created.")
-    serverID = Attribute("Identifies the server that actually hosts data for the record.")
-    shortNames = Attribute("The names for this record.")
-    authIDs = Attribute("Alternative security identities for this record.")
-    fullName = Attribute("The full name of this record.")
-    firstName = Attribute("The first name of this record.")
-    lastName = Attribute("The last name of this record.")
-    emailAddresses = Attribute("The email addresses of this record.")
-    enabledForCalendaring = Attribute("Determines whether this record creates a principal with a calendar home.")
-    enabledForAddressBooks = Attribute("Determines whether this record creates a principal with an address book home.")
-    calendarUserAddresses = Attribute(
-        """
-        An iterable of C{str}s representing calendar user addresses for this
-        L{IDirectoryRecord}.
-
-        A "calendar user address", as defined by U{RFC 2445 section
-        4.3.3<http://xml.resource.org/public/rfc/html/rfc2445.html#anchor50>},
-        is simply an URI which identifies this user.  Some of these URIs are
-        relative references to URLs from the root of the calendar server's HTTP
-        hierarchy.
-        """
-    )
-
-    def members(): #@NoSelf
-        """
-        @return: an iterable of L{IDirectoryRecord}s for the members of this
-            (group) record.
-        """
-
-    def groups(): #@NoSelf
-        """
-        @return: an iterable of L{IDirectoryRecord}s for the groups this
-            record is a member of.
-        """
-
-    def verifyCredentials(credentials): #@NoSelf
-        """
-        Verify that the given credentials can authenticate the principal
-        represented by this record.
-        @param credentials: the credentials to authenticate with.
-        @return: C{True} if the given credentials match this record,
-            C{False} otherwise.
-        """

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/ldapdirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/ldapdirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/ldapdirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,2034 +0,0 @@
-##
-# Copyright (c) 2008-2009 Aymeric Augustin. All rights reserved.
-# Copyright (c) 2006-2014 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.
-##
-
-"""
-LDAP directory service implementation.  Supports principal-property-search
-and restrictToGroup features.
-
-The following attributes from standard schemas are used:
-* Core (RFC 4519):
-    . cn | commonName
-    . givenName
-    . member (if not using NIS groups)
-    . ou
-    . sn | surname
-    . uid | userid (if using NIS groups)
-* COSINE (RFC 4524):
-    . mail
-* InetOrgPerson (RFC 2798):
-    . displayName (if cn is unavailable)
-* NIS (RFC):
-    . gecos (if cn is unavailable)
-    . memberUid (if using NIS groups)
-"""
-
-__all__ = [
-    "LdapDirectoryService",
-]
-
-import ldap.async
-from ldap.filter import escape_filter_chars as ldapEsc
-
-try:
-    # Note: PAM support is currently untested
-    import PAM
-    pamAvailable = True
-except ImportError:
-    pamAvailable = False
-
-import time
-from twisted.cred.credentials import UsernamePassword
-from twistedcaldav.directory.cachingdirectory import (
-    CachingDirectoryService, CachingDirectoryRecord
-)
-from twistedcaldav.directory.directory import DirectoryConfigurationError
-from twistedcaldav.directory.augment import AugmentRecord
-from twistedcaldav.directory.util import splitIntoBatches, normalizeUUID
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
-from twisted.internet.threads import deferToThread
-from twext.python.log import Logger
-from txweb2.http import HTTPError, StatusResponse
-from txweb2 import responsecode
-
-
-
-class LdapDirectoryService(CachingDirectoryService):
-    """
-    LDAP based implementation of L{IDirectoryService}.
-    """
-    log = Logger()
-
-    baseGUID = "5A871574-0C86-44EE-B11B-B9440C3DC4DD"
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (
-            self.__class__.__name__, self.realmName, self.uri
-        )
-
-
-    def __init__(self, params):
-        """
-        @param params: a dictionary containing the following keys:
-            cacheTimeout, realmName, uri, tls, tlsCACertFile, tlsCACertDir,
-            tlsRequireCert, credentials, rdnSchema, groupSchema, resourceSchema
-            poddingSchema
-        """
-
-        defaults = {
-            "augmentService": None,
-            "groupMembershipCache": None,
-            "cacheTimeout": 1,  # Minutes
-            "negativeCaching": False,
-            "warningThresholdSeconds": 3,
-            "batchSize": 500,  # for splitting up large queries
-            "requestTimeoutSeconds": 10,
-            "requestResultsLimit": 200,
-            "optimizeMultiName": False,
-            "queryLocationsImplicitly": True,
-            "restrictEnabledRecords": False,
-            "restrictToGroup": "",
-            "recordTypes": ("users", "groups"),
-            "uri": "ldap://localhost/",
-            "tls": False,
-            "tlsCACertFile": None,
-            "tlsCACertDir": None,
-            "tlsRequireCert": None,  # never, allow, try, demand, hard
-            "credentials": {
-                "dn": None,
-                "password": None,
-            },
-            "authMethod": "LDAP",
-            "rdnSchema": {
-                "base": "dc=example,dc=com",
-                "guidAttr": "entryUUID",
-                "users": {
-                    "rdn": "ou=People",
-                    "filter": None,  # additional filter for this type
-                    "loginEnabledAttr": "",  # attribute controlling login
-                    "loginEnabledValue": "yes",  # "True" value of above attribute
-                    "calendarEnabledAttr": "",  # attribute controlling enabledForCalendaring
-                    "calendarEnabledValue": "yes",  # "True" value of above attribute
-                    "mapping": {  # maps internal record names to LDAP
-                        "recordName": "uid",
-                        "fullName": "cn",
-                        "emailAddresses": ["mail"],  # multiple LDAP fields supported
-                        "firstName": "givenName",
-                        "lastName": "sn",
-                    },
-                },
-                "groups": {
-                    "rdn": "ou=Group",
-                    "filter": None,  # additional filter for this type
-                    "mapping": {  # maps internal record names to LDAP
-                        "recordName": "cn",
-                        "fullName": "cn",
-                        "emailAddresses": ["mail"],  # multiple LDAP fields supported
-                        "firstName": "givenName",
-                        "lastName": "sn",
-                    },
-                },
-                "locations": {
-                    "rdn": "ou=Places",
-                    "filter": None,  # additional filter for this type
-                    "calendarEnabledAttr": "",  # attribute controlling enabledForCalendaring
-                    "calendarEnabledValue": "yes",  # "True" value of above attribute
-                    "associatedAddressAttr": "",
-                    "mapping": {  # maps internal record names to LDAP
-                        "recordName": "cn",
-                        "fullName": "cn",
-                        "emailAddresses": ["mail"],  # multiple LDAP fields supported
-                    },
-                },
-                "resources": {
-                    "rdn": "ou=Resources",
-                    "filter": None,  # additional filter for this type
-                    "calendarEnabledAttr": "",  # attribute controlling enabledForCalendaring
-                    "calendarEnabledValue": "yes",  # "True" value of above attribute
-                    "mapping": {  # maps internal record names to LDAP
-                        "recordName": "cn",
-                        "fullName": "cn",
-                        "emailAddresses": ["mail"],  # multiple LDAP fields supported
-                    },
-                },
-                "addresses": {
-                    "rdn": "ou=Buildings",
-                    "filter": None,  # additional filter for this type
-                    "streetAddressAttr": "",
-                    "geoAttr": "",
-                    "mapping": {  # maps internal record names to LDAP
-                        "recordName": "cn",
-                        "fullName": "cn",
-                    },
-                },
-            },
-            "groupSchema": {
-                "membersAttr": "member",  # how members are specified
-                "nestedGroupsAttr": None,  # how nested groups are specified
-                "memberIdAttr": None,  # which attribute the above refer to (None means use DN)
-            },
-            "resourceSchema": {
-                # Either set this attribute to retrieve the plist version
-                # of resource-info, as in a Leopard OD server, or...
-                "resourceInfoAttr": None,
-                # ...set the above to None and instead specify these
-                # individually:
-                "autoScheduleAttr": None,
-                "autoScheduleEnabledValue": "yes",
-                "proxyAttr": None,  # list of GUIDs
-                "readOnlyProxyAttr": None,  # list of GUIDs
-                "autoAcceptGroupAttr": None,  # single group GUID
-            },
-            "poddingSchema": {
-                "serverIdAttr": None,  # maps to augments server-id
-            },
-        }
-        ignored = None
-        params = self.getParams(params, defaults, ignored)
-
-        self._recordTypes = params["recordTypes"]
-
-        super(LdapDirectoryService, self).__init__(params["cacheTimeout"],
-                                                   params["negativeCaching"])
-
-        self.warningThresholdSeconds = params["warningThresholdSeconds"]
-        self.batchSize = params["batchSize"]
-        self.requestTimeoutSeconds = params["requestTimeoutSeconds"]
-        self.requestResultsLimit = params["requestResultsLimit"]
-        self.optimizeMultiName = params["optimizeMultiName"]
-        if self.batchSize > self.requestResultsLimit:
-            self.batchSize = self.requestResultsLimit
-        self.queryLocationsImplicitly = params["queryLocationsImplicitly"]
-        self.augmentService = params["augmentService"]
-        self.groupMembershipCache = params["groupMembershipCache"]
-        self.realmName = params["uri"]
-        self.uri = params["uri"]
-        self.tls = params["tls"]
-        self.tlsCACertFile = params["tlsCACertFile"]
-        self.tlsCACertDir = params["tlsCACertDir"]
-        self.tlsRequireCert = params["tlsRequireCert"]
-        self.credentials = params["credentials"]
-        self.authMethod = params["authMethod"]
-        self.rdnSchema = params["rdnSchema"]
-        self.groupSchema = params["groupSchema"]
-        self.resourceSchema = params["resourceSchema"]
-        self.poddingSchema = params["poddingSchema"]
-
-        self.base = ldap.dn.str2dn(self.rdnSchema["base"])
-
-        # Certain attributes (such as entryUUID) may be hidden and not
-        # returned by default when queried for all attributes. Therefore it is
-        # necessary to explicitly pass all the possible attributes list
-        # for ldap searches.  Dynamically build the attribute list based on
-        # config.
-        attrSet = set()
-
-        if self.rdnSchema["guidAttr"]:
-            attrSet.add(self.rdnSchema["guidAttr"])
-        for recordType in self.recordTypes():
-            if self.rdnSchema[recordType]["attr"]:
-                attrSet.add(self.rdnSchema[recordType]["attr"])
-            for n in ("calendarEnabledAttr", "associatedAddressAttr",
-                      "streetAddressAttr", "geoAttr"):
-                if self.rdnSchema[recordType].get(n, False):
-                    attrSet.add(self.rdnSchema[recordType][n])
-            for attrList in self.rdnSchema[recordType]["mapping"].values():
-                if attrList:
-                    # Since emailAddresses can map to multiple LDAP fields,
-                    # support either string or list
-                    if isinstance(attrList, str):
-                        attrList = [attrList]
-                    for attr in attrList:
-                        attrSet.add(attr)
-            # Also put the guidAttr attribute into the mappings for each type
-            # so recordsMatchingFields can query on guid
-            self.rdnSchema[recordType]["mapping"]["guid"] = self.rdnSchema["guidAttr"]
-            # Also put the memberIdAttr attribute into the mappings for each type
-            # so recordsMatchingFields can query on memberIdAttr
-            self.rdnSchema[recordType]["mapping"]["memberIdAttr"] = self.groupSchema["memberIdAttr"]
-        if self.groupSchema["membersAttr"]:
-            attrSet.add(self.groupSchema["membersAttr"])
-        if self.groupSchema["nestedGroupsAttr"]:
-            attrSet.add(self.groupSchema["nestedGroupsAttr"])
-        if self.groupSchema["memberIdAttr"]:
-            attrSet.add(self.groupSchema["memberIdAttr"])
-        if self.rdnSchema["users"]["loginEnabledAttr"]:
-            attrSet.add(self.rdnSchema["users"]["loginEnabledAttr"])
-        if self.resourceSchema["resourceInfoAttr"]:
-            attrSet.add(self.resourceSchema["resourceInfoAttr"])
-        if self.resourceSchema["autoScheduleAttr"]:
-            attrSet.add(self.resourceSchema["autoScheduleAttr"])
-        if self.resourceSchema["autoAcceptGroupAttr"]:
-            attrSet.add(self.resourceSchema["autoAcceptGroupAttr"])
-        if self.resourceSchema["proxyAttr"]:
-            attrSet.add(self.resourceSchema["proxyAttr"])
-        if self.resourceSchema["readOnlyProxyAttr"]:
-            attrSet.add(self.resourceSchema["readOnlyProxyAttr"])
-        if self.poddingSchema["serverIdAttr"]:
-            attrSet.add(self.poddingSchema["serverIdAttr"])
-        self.attrlist = list(attrSet)
-
-        self.typeDNs = {}
-        for recordType in self.recordTypes():
-            self.typeDNs[recordType] = ldap.dn.str2dn(
-                self.rdnSchema[recordType]["rdn"].lower()
-            ) + self.base
-
-        self.ldap = None
-
-        # Separate LDAP connection used solely for authenticating clients
-        self.authLDAP = None
-
-        # Restricting access by directory group
-        self.restrictEnabledRecords = params['restrictEnabledRecords']
-        self.restrictToGroup = params['restrictToGroup']
-        self.restrictedTimestamp = 0
-
-
-    def recordTypes(self):
-        return self._recordTypes
-
-
-    def listRecords(self, recordType):
-
-        # Build base for this record Type
-        base = self.typeDNs[recordType]
-
-        # Build filter
-        filterstr = "(!(objectClass=organizationalUnit))"
-        typeFilter = self.rdnSchema[recordType].get("filter", "")
-        if typeFilter:
-            filterstr = "(&%s%s)" % (filterstr, typeFilter)
-
-        # Query the LDAP server
-        self.log.debug(
-            "Querying ldap for records matching base {base} and "
-            "filter {filter} for attributes {attrs}.",
-            base=ldap.dn.dn2str(base), filter=filterstr,
-            attrs=self.attrlist
-        )
-
-        # This takes a while, so if you don't want to have a "long request"
-        # warning logged, use this instead of timedSearch:
-        # results = self.ldap.search_s(ldap.dn.dn2str(base),
-        #     ldap.SCOPE_SUBTREE, filterstr=filterstr, attrlist=self.attrlist)
-        results = self.timedSearch(
-            ldap.dn.dn2str(base), ldap.SCOPE_SUBTREE,
-            filterstr=filterstr, attrlist=self.attrlist
-        )
-
-        records = []
-        numMissingGuids = 0
-        guidAttr = self.rdnSchema["guidAttr"]
-        for dn, attrs in results:
-            dn = normalizeDNstr(dn)
-
-            unrestricted = self.isAllowedByRestrictToGroup(dn, attrs)
-
-            try:
-                record = self._ldapResultToRecord(dn, attrs, recordType)
-                # self.log.debug("Got LDAP record {record}", record=record)
-            except MissingGuidException:
-                numMissingGuids += 1
-                continue
-
-            if not unrestricted:
-                self.log.debug(
-                    "{dn} is not enabled because it's not a member of group: "
-                    "{group}", dn=dn, group=self.restrictToGroup
-                )
-                record.enabledForCalendaring = False
-                record.enabledForAddressBooks = False
-
-            records.append(record)
-
-        if numMissingGuids:
-            self.log.info(
-                "{num} {recordType} records are missing {attr}",
-                num=numMissingGuids, recordType=recordType, attr=guidAttr
-            )
-
-        return records
-
-
-    @inlineCallbacks
-    def recordWithCachedGroupsAlias(self, recordType, alias):
-        """
-        @param recordType: the type of the record to look up.
-        @param alias: the cached-groups alias of the record to look up.
-        @type alias: C{str}
-
-        @return: a deferred L{IDirectoryRecord} with the given cached-groups
-            alias, or C{None} if no such record is found.
-        """
-        memberIdAttr = self.groupSchema["memberIdAttr"]
-        attributeToSearch = "memberIdAttr" if memberIdAttr else "dn"
-
-        fields = [[attributeToSearch, alias, False, "equals"]]
-        results = yield self.recordsMatchingFields(
-            fields, recordType=recordType
-        )
-        if results:
-            returnValue(results[0])
-        else:
-            returnValue(None)
-
-
-    def getExternalProxyAssignments(self):
-        """
-        Retrieve proxy assignments for locations and resources from the
-        directory and return a list of (principalUID, ([memberUIDs)) tuples,
-        suitable for passing to proxyDB.setGroupMembers( )
-        """
-        assignments = []
-
-        guidAttr = self.rdnSchema["guidAttr"]
-        readAttr = self.resourceSchema["readOnlyProxyAttr"]
-        writeAttr = self.resourceSchema["proxyAttr"]
-        if not (guidAttr and readAttr and writeAttr):
-            self.log.error(
-                "LDAP configuration requires guidAttr, proxyAttr, and "
-                "readOnlyProxyAttr in order to use external proxy assignments "
-                "efficiently; falling back to slower method"
-            )
-            # Fall back to the less-specialized version
-            return super(
-                LdapDirectoryService, self
-            ).getExternalProxyAssignments()
-
-        # Build filter
-        filterstr = "(|(%s=*)(%s=*))" % (readAttr, writeAttr)
-        # ...taking into account only calendar-enabled records
-        enabledAttr = self.rdnSchema["locations"]["calendarEnabledAttr"]
-        enabledValue = self.rdnSchema["locations"]["calendarEnabledValue"]
-        if enabledAttr and enabledValue:
-            filterstr = "(&(%s=%s)%s)" % (enabledAttr, enabledValue, filterstr)
-
-        attrlist = [guidAttr, readAttr, writeAttr]
-
-        # Query the LDAP server
-        self.log.debug(
-            "Querying ldap for records matching base {base} and filter "
-            "{filter} for attributes {attrs}.",
-            base=ldap.dn.dn2str(self.base), filter=filterstr,
-            attrs=attrlist
-        )
-
-        results = self.timedSearch(ldap.dn.dn2str(self.base),
-                                   ldap.SCOPE_SUBTREE, filterstr=filterstr,
-                                   attrlist=attrlist)
-
-        for dn, attrs in results:
-            dn = normalizeDNstr(dn)
-            guid = self._getUniqueLdapAttribute(attrs, guidAttr)
-            if guid:
-                guid = normalizeUUID(guid)
-                readDelegate = self._getUniqueLdapAttribute(attrs, readAttr)
-                if readDelegate:
-                    readDelegate = normalizeUUID(readDelegate)
-                    assignments.append(
-                        ("%s#calendar-proxy-read" % (guid,), [readDelegate])
-                    )
-                writeDelegate = self._getUniqueLdapAttribute(attrs, writeAttr)
-                if writeDelegate:
-                    writeDelegate = normalizeUUID(writeDelegate)
-                    assignments.append(
-                        ("%s#calendar-proxy-write" % (guid,), [writeDelegate])
-                    )
-
-        return assignments
-
-
-    def getLDAPConnection(self):
-        if self.ldap is None:
-            self.log.info("Connecting to LDAP {uri}", uri=repr(self.uri))
-            self.ldap = self.createLDAPConnection()
-            self.log.info(
-                "Connection established to LDAP {uri}", uri=repr(self.uri)
-            )
-            if self.credentials.get("dn", ""):
-                try:
-                    self.log.info(
-                        "Binding to LDAP {dn}",
-                        dn=repr(self.credentials.get("dn"))
-                    )
-                    self.ldap.simple_bind_s(
-                        self.credentials.get("dn"),
-                        self.credentials.get("password"),
-                    )
-                    self.log.info(
-                        "Successfully authenticated with LDAP as {dn}",
-                        dn=repr(self.credentials.get("dn"))
-                    )
-                except ldap.INVALID_CREDENTIALS:
-                    self.log.error(
-                        "Can't bind to LDAP {uri}: check credentials",
-                        uri=self.uri
-                    )
-                    raise DirectoryConfigurationError()
-
-        return self.ldap
-
-
-    def createLDAPConnection(self):
-        """
-        Create and configure LDAP connection
-        """
-        cxn = ldap.initialize(self.uri)
-
-        if self.tlsCACertFile:
-            cxn.set_option(ldap.OPT_X_TLS_CACERTFILE, self.tlsCACertFile)
-        if self.tlsCACertDir:
-            cxn.set_option(ldap.OPT_X_TLS_CACERTDIR, self.tlsCACertDir)
-
-        if self.tlsRequireCert == "never":
-            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_NEVER)
-        elif self.tlsRequireCert == "allow":
-            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_ALLOW)
-        elif self.tlsRequireCert == "try":
-            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_TRY)
-        elif self.tlsRequireCert == "demand":
-            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND)
-        elif self.tlsRequireCert == "hard":
-            cxn.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_HARD)
-
-        if self.tls:
-            cxn.start_tls_s()
-
-        return cxn
-
-
-    def authenticate(self, dn, password):
-        """
-        Perform simple bind auth, raising ldap.INVALID_CREDENTIALS if
-        bad password
-        """
-        TRIES = 3
-
-        for _ignore_i in xrange(TRIES):
-            self.log.debug("Authenticating {dn}", dn=dn)
-
-            if self.authLDAP is None:
-                self.log.debug("Creating authentication connection to LDAP")
-                self.authLDAP = self.createLDAPConnection()
-
-            try:
-                startTime = time.time()
-                self.authLDAP.simple_bind_s(dn, password)
-                # Getting here means success, so break the retry loop
-                break
-
-            except ldap.INAPPROPRIATE_AUTH:
-                # Seen when using an empty password, treat as invalid creds
-                raise ldap.INVALID_CREDENTIALS()
-
-            except ldap.NO_SUCH_OBJECT:
-                self.log.error(
-                    "LDAP Authentication error for {dn}: NO_SUCH_OBJECT",
-                    dn=dn
-                )
-                # fall through to try again; could be transient
-
-            except ldap.INVALID_CREDENTIALS:
-                raise
-
-            except ldap.SERVER_DOWN:
-                self.log.error("Lost connection to LDAP server.")
-                self.authLDAP = None
-                # Fall through and retry if TRIES has been reached
-
-            except Exception, e:
-                self.log.error(
-                    "LDAP authentication failed with {e}.", e=e
-                )
-                raise
-
-            finally:
-                totalTime = time.time() - startTime
-                if totalTime > self.warningThresholdSeconds:
-                    self.log.error(
-                        "LDAP auth exceeded threshold: {time:.2f} seconds for "
-                        "{dn}", time=totalTime, dn=dn
-                    )
-
-        else:
-            self.log.error(
-                "Giving up on LDAP authentication after {count:d} tries.  "
-                "Responding with 503.", count=TRIES
-            )
-            raise HTTPError(StatusResponse(
-                responsecode.SERVICE_UNAVAILABLE, "LDAP server unavailable"
-            ))
-
-        self.log.debug("Authentication succeeded for {dn}", dn=dn)
-
-
-    def timedSearch(
-        self, base, scope, filterstr="(objectClass=*)", attrlist=None,
-        timeoutSeconds=-1, resultLimit=0
-    ):
-        """
-        Execute an LDAP query, retrying up to 3 times in case the LDAP server
-        has gone down and we need to reconnect. If it takes longer than the
-        configured threshold, emit a log error.
-        The number of records requested is controlled by resultLimit (0=no
-        limit).
-        If timeoutSeconds is not -1, the query will abort after the specified
-        number of seconds and the results retrieved so far are returned.
-        """
-        TRIES = 3
-
-        for i in xrange(TRIES):
-            try:
-                s = ldap.async.List(self.getLDAPConnection())
-                s.startSearch(
-                    base, scope, filterstr, attrList=attrlist,
-                    timeout=timeoutSeconds, sizelimit=resultLimit
-                )
-                startTime = time.time()
-                s.processResults()
-            except ldap.NO_SUCH_OBJECT:
-                return []
-            except ldap.FILTER_ERROR, e:
-                self.log.error(
-                    "LDAP filter error: {e} {filter}", e=e, filter=filterstr
-                )
-                return []
-            except ldap.SIZELIMIT_EXCEEDED, e:
-                self.log.debug(
-                    "LDAP result limit exceeded: {limit:d}", limit=resultLimit
-                )
-            except ldap.TIMELIMIT_EXCEEDED, e:
-                self.log.warn(
-                    "LDAP timeout exceeded: {t:d} seconds", t=timeoutSeconds
-                )
-            except ldap.SERVER_DOWN:
-                self.ldap = None
-                self.log.error(
-                    "LDAP server unavailable (tried {count:d} times)",
-                    count=(i + 1)
-                )
-                continue
-
-            # change format, ignoring resultsType
-            result = [
-                resultItem for _ignore_resultType, resultItem in s.allResults
-            ]
-
-            totalTime = time.time() - startTime
-            if totalTime > self.warningThresholdSeconds:
-                if filterstr and len(filterstr) > 100:
-                    filterstr = "%s..." % (filterstr[:100],)
-                self.log.error(
-                    "LDAP query exceeded threshold: {time:.2f} seconds for "
-                    "{base} {filter} {attrs} (#results={count:d})",
-                    time=totalTime, base=base, filter=filterstr,
-                    attrs=attrlist, count=len(result),
-                )
-            return result
-
-        raise HTTPError(StatusResponse(
-            responsecode.SERVICE_UNAVAILABLE, "LDAP server unavailable"
-        ))
-
-
-    def isAllowedByRestrictToGroup(self, dn, attrs):
-        """
-        Check to see if the principal with the given DN and LDAP attributes is
-        a member of the restrictToGroup.
-
-        @param dn: an LDAP dn
-        @type dn: C{str}
-        @param attrs: LDAP attributes
-        @type attrs: C{dict}
-        @return: True if principal is in the group (or restrictEnabledRecords if turned off).
-        @rtype: C{boolean}
-        """
-        if not self.restrictEnabledRecords:
-            return True
-        if self.groupSchema["memberIdAttr"]:
-            value = self._getUniqueLdapAttribute(
-                attrs, self.groupSchema["memberIdAttr"]
-            )
-        else:  # No memberIdAttr implies DN
-            value = dn
-        return value in self.restrictedPrincipals
-
-
-    @property
-    def restrictedPrincipals(self):
-        """
-        Look up (and cache) the set of guids that are members of the
-        restrictToGroup.  If restrictToGroup is not set, return None to
-        indicate there are no group restrictions.
-        """
-        if self.restrictEnabledRecords:
-
-            if time.time() - self.restrictedTimestamp > self.cacheTimeout:
-                # fault in the members of group of name self.restrictToGroup
-                recordType = self.recordType_groups
-                base = self.typeDNs[recordType]
-                # TODO: This shouldn't be hardcoded to cn
-                filterstr = "(cn=%s)" % (self.restrictToGroup,)
-                self.log.debug(
-                    "Retrieving ldap record with base {base} and filter "
-                    "{filter}.",
-                    base=ldap.dn.dn2str(base), filter=filterstr
-                )
-                result = self.timedSearch(
-                    ldap.dn.dn2str(base),
-                    ldap.SCOPE_SUBTREE,
-                    filterstr=filterstr,
-                    attrlist=self.attrlist
-                )
-
-                members = []
-                nestedGroups = []
-
-                if len(result) == 1:
-                    dn, attrs = result[0]
-                    dn = normalizeDNstr(dn)
-                    if self.groupSchema["membersAttr"]:
-                        members = self._getMultipleLdapAttributes(
-                            attrs,
-                            self.groupSchema["membersAttr"]
-                        )
-                        if not self.groupSchema["memberIdAttr"]:  # DNs
-                            members = [normalizeDNstr(m) for m in members]
-                        members = set(members)
-
-                    if self.groupSchema["nestedGroupsAttr"]:
-                        nestedGroups = self._getMultipleLdapAttributes(
-                            attrs,
-                            self.groupSchema["nestedGroupsAttr"]
-                        )
-                        if not self.groupSchema["memberIdAttr"]:  # DNs
-                            nestedGroups = [
-                                normalizeDNstr(g) for g in nestedGroups
-                            ]
-                        nestedGroups = set(nestedGroups)
-                    else:
-                        # Since all members are lumped into the same attribute,
-                        # treat them all as nestedGroups instead
-                        nestedGroups = members
-                        members = set()
-
-                self._cachedRestrictedPrincipals = set(
-                    self._expandGroupMembership(members, nestedGroups)
-                )
-                self.log.info(
-                    "Got {count} restricted group members",
-                    count=len(self._cachedRestrictedPrincipals)
-                )
-                self.restrictedTimestamp = time.time()
-            return self._cachedRestrictedPrincipals
-        else:
-            # No restrictions
-            return None
-
-
-    def _expandGroupMembership(self, members, nestedGroups, processedItems=None):
-        """
-        A generator which recursively yields principals which are included within nestedGroups
-
-        @param members:  If the LDAP service is configured to use different attributes to
-            indicate member users and member nested groups, members will include the non-groups.
-            Otherwise, members will be empty and only nestedGroups will be used.
-        @type members: C{set}
-        @param nestedGroups:  If the LDAP service is configured to use different attributes to
-            indicate member users and member nested groups, nestedGroups will include only
-            the groups; otherwise nestedGroups will include all members
-        @type members: C{set}
-        @param processedItems: The set of members that have already been looked up in LDAP
-            so the code doesn't have to look up the same member twice or get stuck in a
-            membership loop.
-        @type processedItems: C{set}
-        @return: All members of the group, the values will correspond to memberIdAttr
-            if memberIdAttr is set in the group schema, or DNs otherwise.
-        @rtype: generator of C{str}
-        """
-
-        if processedItems is None:
-            processedItems = set()
-
-        if isinstance(members, str):
-            members = [members]
-
-        if isinstance(nestedGroups, str):
-            nestedGroups = [nestedGroups]
-
-        for member in members:
-            if member not in processedItems:
-                processedItems.add(member)
-                yield member
-
-        for group in nestedGroups:
-            if group in processedItems:
-                continue
-
-            recordType = self.recordType_groups
-            base = self.typeDNs[recordType]
-            if self.groupSchema["memberIdAttr"]:
-                scope = ldap.SCOPE_SUBTREE
-                base = self.typeDNs[recordType]
-                filterstr = "(%s=%s)" % (self.groupSchema["memberIdAttr"], group)
-            else:  # Use DN
-                scope = ldap.SCOPE_BASE
-                base = ldap.dn.str2dn(group)
-                filterstr = "(objectClass=*)"
-
-            self.log.debug(
-                "Retrieving ldap record with base {base} and filter {filter}.",
-                base=ldap.dn.dn2str(base), filter=filterstr
-            )
-            result = self.timedSearch(ldap.dn.dn2str(base),
-                                      scope,
-                                      filterstr=filterstr,
-                                      attrlist=self.attrlist)
-
-            if len(result) == 0:
-                continue
-
-            subMembers = set()
-            subNestedGroups = set()
-            if len(result) == 1:
-                dn, attrs = result[0]
-                dn = normalizeDNstr(dn)
-                if self.groupSchema["membersAttr"]:
-                    subMembers = self._getMultipleLdapAttributes(
-                        attrs,
-                        self.groupSchema["membersAttr"]
-                    )
-                    if not self.groupSchema["memberIdAttr"]:  # these are DNs
-                        subMembers = [normalizeDNstr(m) for m in subMembers]
-                    subMembers = set(subMembers)
-
-                if self.groupSchema["nestedGroupsAttr"]:
-                    subNestedGroups = self._getMultipleLdapAttributes(
-                        attrs,
-                        self.groupSchema["nestedGroupsAttr"]
-                    )
-                    if not self.groupSchema["memberIdAttr"]:  # these are DNs
-                        subNestedGroups = [normalizeDNstr(g) for g in subNestedGroups]
-                    subNestedGroups = set(subNestedGroups)
-
-            processedItems.add(group)
-            yield group
-
-            for item in self._expandGroupMembership(subMembers,
-                                                    subNestedGroups,
-                                                    processedItems):
-                yield item
-
-
-    def _getUniqueLdapAttribute(self, attrs, *keys):
-        """
-        Get the first value for one or several attributes
-        Useful when attributes have aliases (e.g. sn vs. surname)
-        """
-        for key in keys:
-            values = attrs.get(key)
-            if values is not None:
-                return values[0]
-        return None
-
-
-    def _getMultipleLdapAttributes(self, attrs, *keys):
-        """
-        Get all values for one or several attributes
-        """
-        results = []
-        for key in keys:
-            if key:
-                values = attrs.get(key)
-                if values is not None:
-                    results += values
-        return results
-
-
-    def _ldapResultToRecord(self, dn, attrs, recordType):
-        """
-        Convert the attrs returned by a LDAP search into a LdapDirectoryRecord
-        object.
-
-        If guidAttr was specified in the config but is missing from attrs,
-        raises MissingGuidException
-        """
-
-        guid = None
-        authIDs = set()
-        fullName = None
-        firstName = ""
-        lastName = ""
-        emailAddresses = set()
-        enabledForCalendaring = None
-        enabledForAddressBooks = None
-        uid = None
-        enabledForLogin = True
-        extras = {}
-
-        shortNames = tuple(self._getMultipleLdapAttributes(attrs, self.rdnSchema[recordType]["mapping"]["recordName"]))
-        if not shortNames:
-            raise MissingRecordNameException()
-
-        # First check for and add guid
-        guidAttr = self.rdnSchema["guidAttr"]
-        if guidAttr:
-            guid = self._getUniqueLdapAttribute(attrs, guidAttr)
-            if not guid:
-                self.log.debug(
-                    "LDAP data for {shortNames} is missing guid attribute "
-                    "{attr}",
-                    shortNames=shortNames, attr=guidAttr
-                )
-                raise MissingGuidException()
-            guid = normalizeUUID(guid)
-
-        # Find or build email
-        # (The emailAddresses mapping is a list of ldap fields)
-        emailAddressesMappedTo = self.rdnSchema[recordType]["mapping"].get("emailAddresses", "")
-        # Supporting either string or list for emailAddresses:
-        if isinstance(emailAddressesMappedTo, str):
-            emailAddresses = set(self._getMultipleLdapAttributes(attrs, self.rdnSchema[recordType]["mapping"].get("emailAddresses", "")))
-        else:
-            emailAddresses = set(self._getMultipleLdapAttributes(attrs, *self.rdnSchema[recordType]["mapping"]["emailAddresses"]))
-        emailSuffix = self.rdnSchema[recordType].get("emailSuffix", None)
-
-        if len(emailAddresses) == 0 and emailSuffix:
-            emailPrefix = self._getUniqueLdapAttribute(
-                attrs,
-                self.rdnSchema[recordType].get("attr", "cn")
-            )
-            emailAddresses.add(emailPrefix + emailSuffix)
-
-        proxyGUIDs = ()
-        readOnlyProxyGUIDs = ()
-        autoSchedule = False
-        autoAcceptGroup = ""
-        memberGUIDs = []
-
-        # LDAP attribute -> principal matchings
-        if recordType == self.recordType_users:
-            fullName = self._getUniqueLdapAttribute(attrs, self.rdnSchema[recordType]["mapping"]["fullName"])
-            firstName = self._getUniqueLdapAttribute(attrs, self.rdnSchema[recordType]["mapping"]["firstName"])
-            lastName = self._getUniqueLdapAttribute(attrs, self.rdnSchema[recordType]["mapping"]["lastName"])
-            enabledForCalendaring = True
-            enabledForAddressBooks = True
-
-        elif recordType == self.recordType_groups:
-            fullName = self._getUniqueLdapAttribute(attrs, self.rdnSchema[recordType]["mapping"]["fullName"])
-            enabledForCalendaring = False
-            enabledForAddressBooks = False
-            enabledForLogin = False
-
-            if self.groupSchema["membersAttr"]:
-                members = self._getMultipleLdapAttributes(attrs, self.groupSchema["membersAttr"])
-                memberGUIDs.extend(members)
-            if self.groupSchema["nestedGroupsAttr"]:
-                members = self._getMultipleLdapAttributes(attrs, self.groupSchema["nestedGroupsAttr"])
-                memberGUIDs.extend(members)
-
-            # Normalize members if they're in DN form
-            if not self.groupSchema["memberIdAttr"]:  # empty = dn
-                guids = list(memberGUIDs)
-                memberGUIDs = []
-                for dnStr in guids:
-                    try:
-                        dnStr = normalizeDNstr(dnStr)
-                        memberGUIDs.append(dnStr)
-                    except Exception, e:
-                        # LDAP returned an illegal DN value, log and ignore it
-                        self.log.warn("Bad LDAP DN: {dn!r}", dn=dnStr)
-
-        elif recordType in (self.recordType_resources,
-                            self.recordType_locations):
-            fullName = self._getUniqueLdapAttribute(attrs, self.rdnSchema[recordType]["mapping"]["fullName"])
-            enabledForCalendaring = True
-            enabledForAddressBooks = False
-            enabledForLogin = False
-            if self.resourceSchema["resourceInfoAttr"]:
-                resourceInfo = self._getUniqueLdapAttribute(
-                    attrs,
-                    self.resourceSchema["resourceInfoAttr"]
-                )
-                if resourceInfo:
-                    try:
-                        (
-                            autoSchedule,
-                            proxy,
-                            readOnlyProxy,
-                            autoAcceptGroup
-                        ) = self.parseResourceInfo(
-                            resourceInfo,
-                            guid,
-                            recordType,
-                            shortNames[0]
-                        )
-                        if proxy:
-                            proxyGUIDs = (proxy,)
-                        if readOnlyProxy:
-                            readOnlyProxyGUIDs = (readOnlyProxy,)
-                    except ValueError, e:
-                        self.log.error(
-                            "Unable to parse resource info: {e}", e=e
-                        )
-            else:  # the individual resource attributes might be specified
-                if self.resourceSchema["autoScheduleAttr"]:
-                    autoScheduleValue = self._getUniqueLdapAttribute(
-                        attrs,
-                        self.resourceSchema["autoScheduleAttr"]
-                    )
-                    autoSchedule = (
-                        autoScheduleValue == self.resourceSchema["autoScheduleEnabledValue"]
-                    )
-                if self.resourceSchema["proxyAttr"]:
-                    proxyGUIDs = set(
-                        self._getMultipleLdapAttributes(
-                            attrs,
-                            self.resourceSchema["proxyAttr"]
-                        )
-                    )
-                if self.resourceSchema["readOnlyProxyAttr"]:
-                    readOnlyProxyGUIDs = set(
-                        self._getMultipleLdapAttributes(
-                            attrs,
-                            self.resourceSchema["readOnlyProxyAttr"]
-                        )
-                    )
-                if self.resourceSchema["autoAcceptGroupAttr"]:
-                    autoAcceptGroup = self._getUniqueLdapAttribute(
-                        attrs,
-                        self.resourceSchema["autoAcceptGroupAttr"]
-                    )
-
-            if recordType == self.recordType_locations:
-                if self.rdnSchema[recordType].get("associatedAddressAttr", ""):
-                    associatedAddress = self._getUniqueLdapAttribute(
-                        attrs,
-                        self.rdnSchema[recordType]["associatedAddressAttr"]
-                    )
-                    if associatedAddress:
-                        extras["associatedAddress"] = associatedAddress
-
-        elif recordType == self.recordType_addresses:
-            if self.rdnSchema[recordType].get("geoAttr", ""):
-                geo = self._getUniqueLdapAttribute(
-                    attrs,
-                    self.rdnSchema[recordType]["geoAttr"]
-                )
-                if geo:
-                    extras["geo"] = geo
-            if self.rdnSchema[recordType].get("streetAddressAttr", ""):
-                street = self._getUniqueLdapAttribute(
-                    attrs,
-                    self.rdnSchema[recordType]["streetAddressAttr"]
-                )
-                if street:
-                    extras["streetAddress"] = street
-
-        serverID = None
-        if self.poddingSchema["serverIdAttr"]:
-            serverID = self._getUniqueLdapAttribute(
-                attrs,
-                self.poddingSchema["serverIdAttr"]
-            )
-
-        record = LdapDirectoryRecord(
-            service=self,
-            recordType=recordType,
-            guid=guid,
-            shortNames=shortNames,
-            authIDs=authIDs,
-            fullName=fullName,
-            firstName=firstName,
-            lastName=lastName,
-            emailAddresses=emailAddresses,
-            uid=uid,
-            dn=dn,
-            memberGUIDs=memberGUIDs,
-            extProxies=proxyGUIDs,
-            extReadOnlyProxies=readOnlyProxyGUIDs,
-            attrs=attrs,
-            **extras
-        )
-
-        if self.augmentService is not None:
-            # 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 = self.augmentService.getAugmentRecord(record.guid, recordType)
-            d.addCallback(lambda x: record.addAugmentInformation(x))
-
-        else:
-            # Generate augment record based on information retrieved from LDAP
-            augmentRecord = AugmentRecord(
-                guid,
-                enabled=True,
-                serverID=serverID,
-                enabledForCalendaring=enabledForCalendaring,
-                autoSchedule=autoSchedule,
-                autoAcceptGroup=autoAcceptGroup,
-                enabledForAddressBooks=enabledForAddressBooks,  # TODO: add to LDAP?
-                enabledForLogin=enabledForLogin,
-            )
-            record.addAugmentInformation(augmentRecord)
-
-        # Override with LDAP login control if attribute specified
-        if recordType == self.recordType_users:
-            loginEnabledAttr = self.rdnSchema[recordType]["loginEnabledAttr"]
-            if loginEnabledAttr:
-                loginEnabledValue = self.rdnSchema[recordType]["loginEnabledValue"]
-                record.enabledForLogin = self._getUniqueLdapAttribute(
-                    attrs, loginEnabledAttr
-                ) == loginEnabledValue
-
-        # Override with LDAP calendar-enabled control if attribute specified
-        calendarEnabledAttr = self.rdnSchema[recordType].get("calendarEnabledAttr", "")
-        if calendarEnabledAttr:
-            calendarEnabledValue = self.rdnSchema[recordType]["calendarEnabledValue"]
-            record.enabledForCalendaring = self._getUniqueLdapAttribute(
-                attrs,
-                calendarEnabledAttr
-            ) == calendarEnabledValue
-
-        return record
-
-
-    def queryDirectory(
-        self, recordTypes, indexType, indexKey, queryMethod=None
-    ):
-        """
-        Queries the LDAP directory for the record which has an attribute value
-        matching the indexType and indexKey parameters.
-
-        recordTypes is a list of record types to limit the search to.
-        indexType specifies one of the CachingDirectoryService constants
-            identifying which attribute to search on.
-        indexKey is the value to search for.
-
-        Nothing is returned -- the resulting record (if any) is placed in
-        the cache.
-        """
-
-        if queryMethod is None:
-            queryMethod = self.timedSearch
-
-        self.log.debug(
-            "LDAP query for types {types}, indexType {indexType} and "
-            "indexKey {indexKey}",
-            types=recordTypes, indexType=indexType, indexKey=indexKey
-        )
-
-        guidAttr = self.rdnSchema["guidAttr"]
-        for recordType in recordTypes:
-            # Build base for this record Type
-            base = self.typeDNs[recordType]
-
-            # Build filter
-            filterstr = "(!(objectClass=organizationalUnit))"
-            typeFilter = self.rdnSchema[recordType].get("filter", "")
-            if typeFilter:
-                filterstr = "(&%s%s)" % (filterstr, typeFilter)
-
-            if indexType == self.INDEX_TYPE_GUID:
-                # Query on guid only works if guid attribute has been defined.
-                # Support for query on guid even if is auto-generated should
-                # be added.
-                if not guidAttr:
-                    return
-                filterstr = "(&%s(%s=%s))" % (filterstr, guidAttr, indexKey)
-
-            elif indexType == self.INDEX_TYPE_SHORTNAME:
-                filterstr = "(&%s(%s=%s))" % (
-                    filterstr,
-                    self.rdnSchema[recordType]["mapping"]["recordName"],
-                    ldapEsc(indexKey)
-                )
-
-            elif indexType == self.INDEX_TYPE_CUA:
-                # indexKey is of the form "mailto:test at example.net"
-                email = indexKey[7:]  # strip "mailto:"
-                emailSuffix = self.rdnSchema[recordType].get(
-                    "emailSuffix", None
-                )
-                if (
-                    emailSuffix is not None and
-                    email.partition("@")[2] == emailSuffix
-                ):
-                    filterstr = "(&%s(|(&(!(mail=*))(%s=%s))(mail=%s)))" % (
-                        filterstr,
-                        self.rdnSchema[recordType].get("attr", "cn"),
-                        email.partition("@")[0],
-                        ldapEsc(email)
-                    )
-                else:
-                    # emailAddresses can map to multiple LDAP fields
-                    ldapFields = self.rdnSchema[recordType]["mapping"].get(
-                        "emailAddresses", ""
-                    )
-                    if isinstance(ldapFields, str):
-                        if ldapFields:
-                            subfilter = (
-                                "(%s=%s)" % (ldapFields, ldapEsc(email))
-                            )
-                        else:
-                            # No LDAP attribute assigned for emailAddresses
-                            continue
-
-                    else:
-                        subfilter = []
-                        for ldapField in ldapFields:
-                            if ldapField:
-                                subfilter.append(
-                                    "(%s=%s)" % (ldapField, ldapEsc(email))
-                                )
-                        if not subfilter:
-                            # No LDAP attribute assigned for emailAddresses
-                            continue
-
-                        subfilter = "(|%s)" % ("".join(subfilter))
-                    filterstr = "(&%s%s)" % (filterstr, subfilter)
-
-            elif indexType == self.INDEX_TYPE_AUTHID:
-                return
-
-            # Query the LDAP server
-            self.log.debug(
-                "Retrieving ldap record with base %s and filter %s.",
-                base=ldap.dn.dn2str(base), filter=filterstr,
-            )
-            result = queryMethod(
-                ldap.dn.dn2str(base),
-                ldap.SCOPE_SUBTREE,
-                filterstr=filterstr,
-                attrlist=self.attrlist,
-            )
-
-            if result:
-                dn, attrs = result.pop()
-                dn = normalizeDNstr(dn)
-
-                unrestricted = self.isAllowedByRestrictToGroup(dn, attrs)
-
-                try:
-                    record = self._ldapResultToRecord(dn, attrs, recordType)
-                    self.log.debug("Got LDAP record {rec}", rec=record)
-
-                    if not unrestricted:
-                        self.log.debug(
-                            "{dn} is not enabled because it's not a member of "
-                            "group {group!r}",
-                            dn=dn, group=self.restrictToGroup
-                        )
-                        record.enabledForCalendaring = False
-                        record.enabledForAddressBooks = False
-
-                    record.applySACLs()
-
-                    self.recordCacheForType(recordType).addRecord(
-                        record, indexType, indexKey
-                    )
-
-                    # We got a match, so don't bother checking other types
-                    break
-
-                except MissingRecordNameException:
-                    self.log.warn(
-                        "Ignoring record missing record name "
-                        "attribute: recordType {recordType}, indexType "
-                        "{indexType} and indexKey {indexKey}",
-                        recordTypes=recordTypes, indexType=indexType,
-                        indexKey=indexKey,
-                    )
-
-                except MissingGuidException:
-                    self.log.warn(
-                        "Ignoring record missing guid attribute: "
-                        "recordType {recordType}, indexType {indexType} and "
-                        "indexKey {indexKey}",
-                        recordTypes=recordTypes, indexType=indexType,
-                        indexKey=indexKey
-                    )
-
-
-    def recordsMatchingTokens(self, tokens, context=None, limitResults=50, timeoutSeconds=10):
-        """
-        # TODO: hook up limitResults to the client limit in the query
-
-        @param tokens: The tokens to search on
-        @type tokens: C{list} of C{str} (utf-8 bytes)
-        @param context: An indication of what the end user is searching
-            for; "attendee", "location", or None
-        @type context: C{str}
-        @return: a deferred sequence of L{IDirectoryRecord}s which
-            match the given tokens and optional context.
-
-        Each token is searched for within each record's full name and
-        email address; if each token is found within a record that
-        record is returned in the results.
-
-        If context is None, all record types are considered.  If
-        context is "location", only locations are considered.  If
-        context is "attendee", only users, groups, and resources
-        are considered.
-        """
-        self.log.debug(
-            "Peforming calendar user search for {tokens} ({context})",
-            tokens=tokens, context=context
-        )
-        startTime = time.time()
-        records = []
-        recordTypes = self.recordTypesForSearchContext(context)
-        recordTypes = [r for r in recordTypes if r in self.recordTypes()]
-
-        typeCounts = {}
-        for recordType in recordTypes:
-            if limitResults == 0:
-                self.log.debug("LDAP search aggregate limit reached")
-                break
-            typeCounts[recordType] = 0
-            base = self.typeDNs[recordType]
-            scope = ldap.SCOPE_SUBTREE
-            extraFilter = self.rdnSchema[recordType].get("filter", "")
-            filterstr = buildFilterFromTokens(
-                recordType,
-                self.rdnSchema[recordType]["mapping"],
-                tokens,
-                extra=extraFilter
-            )
-
-            if filterstr is not None:
-                # Query the LDAP server
-                self.log.debug(
-                    "LDAP search {base} {filter} (limit={limit:d})",
-                    base=ldap.dn.dn2str(base), filter=filterstr,
-                    limit=limitResults,
-                )
-                results = self.timedSearch(
-                    ldap.dn.dn2str(base),
-                    scope,
-                    filterstr=filterstr,
-                    attrlist=self.attrlist,
-                    timeoutSeconds=timeoutSeconds,
-                    resultLimit=limitResults
-                )
-                numMissingGuids = 0
-                numMissingRecordNames = 0
-                numNotEnabled = 0
-                for dn, attrs in results:
-                    dn = normalizeDNstr(dn)
-                    # Skip if group restriction is in place and guid is not
-                    # a member
-                    if (
-                            recordType != self.recordType_groups and
-                            not self.isAllowedByRestrictToGroup(dn, attrs)
-                    ):
-                        continue
-
-                    try:
-                        record = self._ldapResultToRecord(dn, attrs, recordType)
-
-                        # For non-group records, if not enabled for calendaring do
-                        # not include in principal property search results
-                        if (recordType != self.recordType_groups):
-                            if not record.enabledForCalendaring:
-                                numNotEnabled += 1
-                                continue
-
-                        records.append(record)
-                        typeCounts[recordType] += 1
-                        limitResults -= 1
-
-                    except MissingGuidException:
-                        numMissingGuids += 1
-
-                    except MissingRecordNameException:
-                        numMissingRecordNames += 1
-
-                self.log.debug(
-                    "LDAP search returned {resultCount:d} results, "
-                    "{typeCount:d} usable",
-                    resultCount=len(results), typeCount=typeCounts[recordType]
-                )
-
-        typeCountsStr = ", ".join(
-            ["%s:%d" % (rt, ct) for (rt, ct) in typeCounts.iteritems()]
-        )
-        totalTime = time.time() - startTime
-        self.log.info(
-            "Calendar user search for {tokens} matched {recordCount:d} "
-            "records ({typeCount}) in {time!.2f} seconds",
-            tokens=tokens, recordCount=len(records),
-            typeCount=typeCountsStr, time=totalTime,
-        )
-        return succeed(records)
-
-
-    @inlineCallbacks
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-        """
-        Carries out the work of a principal-property-search against LDAP
-        Returns a deferred list of directory records.
-        """
-        records = []
-
-        self.log.debug(
-            "Performing principal property search for {fields}", fields=fields
-        )
-
-        if recordType is None:
-            # Make a copy since we're modifying it
-            recordTypes = list(self.recordTypes())
-
-            # principal-property-search syntax doesn't provide a way to ask
-            # for 3 of the 4 types (either all types or a single type).  This
-            # is wasteful in the case of iCal looking for event attendees
-            # since it always ignores the locations.  This config flag lets
-            # you skip querying for locations in this case:
-            if not self.queryLocationsImplicitly:
-                if self.recordType_locations in recordTypes:
-                    recordTypes.remove(self.recordType_locations)
-        else:
-            recordTypes = [recordType]
-
-        guidAttr = self.rdnSchema["guidAttr"]
-        for recordType in recordTypes:
-
-            base = self.typeDNs[recordType]
-
-            if fields[0][0] == "dn":
-                # DN's are not an attribute that can be searched on by filter
-                scope = ldap.SCOPE_BASE
-                filterstr = "(objectClass=*)"
-                base = ldap.dn.str2dn(fields[0][1])
-
-            else:
-                scope = ldap.SCOPE_SUBTREE
-                filterstr = buildFilter(
-                    recordType,
-                    self.rdnSchema[recordType]["mapping"],
-                    fields,
-                    operand=operand,
-                    optimizeMultiName=self.optimizeMultiName
-                )
-
-            if filterstr is not None:
-                # Query the LDAP server
-                self.log.debug(
-                    "LDAP search {base} {scope} {filter}",
-                    base=ldap.dn.dn2str(base), scope=scope, filter=filterstr
-                )
-                results = (yield deferToThread(
-                    self.timedSearch,
-                    ldap.dn.dn2str(base),
-                    scope,
-                    filterstr=filterstr,
-                    attrlist=self.attrlist,
-                    timeoutSeconds=self.requestTimeoutSeconds,
-                    resultLimit=self.requestResultsLimit)
-                )
-                self.log.debug(
-                    "LDAP search returned {count} results", count=len(results)
-                )
-                numMissingGuids = 0
-                numMissingRecordNames = 0
-                for dn, attrs in results:
-                    dn = normalizeDNstr(dn)
-                    # Skip if group restriction is in place and guid is not
-                    # a member
-                    if (
-                        recordType != self.recordType_groups and
-                        not self.isAllowedByRestrictToGroup(dn, attrs)
-                    ):
-                        continue
-
-                    try:
-                        record = self._ldapResultToRecord(dn, attrs, recordType)
-
-                        # For non-group records, if not enabled for calendaring do
-                        # not include in principal property search results
-                        if (recordType != self.recordType_groups):
-                            if not record.enabledForCalendaring:
-                                continue
-
-                        records.append(record)
-
-                    except MissingGuidException:
-                        numMissingGuids += 1
-
-                    except MissingRecordNameException:
-                        numMissingRecordNames += 1
-
-                if numMissingGuids:
-                    self.log.warn(
-                        "{count:d} {type} records are missing {attr}",
-                        count=numMissingGuids, type=recordType, attr=guidAttr
-                    )
-
-                if numMissingRecordNames:
-                    self.log.warn(
-                        "{count:d} {type} records are missing record name",
-                        count=numMissingRecordNames, type=recordType,
-                    )
-
-        self.log.debug(
-            "Principal property search matched {count} records",
-            count=len(records)
-        )
-        returnValue(records)
-
-
-    @inlineCallbacks
-    def getGroups(self, guids):
-        """
-        Returns a set of group records for the list of guids passed in.  For
-        any group that also contains subgroups, those subgroups' records are
-        also returned, and so on.
-        """
-
-        recordsByAlias = {}
-
-        groupsDN = self.typeDNs[self.recordType_groups]
-        memberIdAttr = self.groupSchema["memberIdAttr"]
-
-        # First time through the loop we search using the attribute
-        # corresponding to guid, since that is what the proxydb uses.
-        # Subsequent iterations fault in groups via the attribute
-        # used to identify members.
-        attributeToSearch = "guid"
-        valuesToFetch = guids
-
-        while valuesToFetch:
-            results = []
-
-            if attributeToSearch == "dn":
-                # Since DN can't be searched on in a filter we have to call
-                # recordsMatchingFields for *each* DN.
-                for value in valuesToFetch:
-                    fields = [["dn", value, False, "equals"]]
-                    result = (
-                        yield self.recordsMatchingFields(
-                            fields,
-                            recordType=self.recordType_groups
-                        )
-                    )
-                    results.extend(result)
-            else:
-                for batch in splitIntoBatches(valuesToFetch, self.batchSize):
-                    fields = []
-                    for value in batch:
-                        fields.append([attributeToSearch, value, False, "equals"])
-                    result = (
-                        yield self.recordsMatchingFields(
-                            fields,
-                            recordType=self.recordType_groups
-                        )
-                    )
-                    results.extend(result)
-
-            # Reset values for next iteration
-            valuesToFetch = set()
-
-            for record in results:
-                alias = record.cachedGroupsAlias()
-                if alias not in recordsByAlias:
-                    recordsByAlias[alias] = record
-
-                # record.memberGUIDs() contains the members of this group,
-                # but it might not be in guid form; it will be data from
-                # self.groupSchema["memberIdAttr"]
-                for memberAlias in record.memberGUIDs():
-                    if not memberIdAttr:
-                        # Members are identified by dn so we can take a short
-                        # cut:  we know we only need to examine groups, and
-                        # those will be children of the groups DN
-                        if not dnContainedIn(ldap.dn.str2dn(memberAlias),
-                                             groupsDN):
-                            continue
-                    if memberAlias not in recordsByAlias:
-                        valuesToFetch.add(memberAlias)
-
-            # Switch to the LDAP attribute used for identifying members
-            # for subsequent iterations.  If memberIdAttr is not specified
-            # in the config, we'll search using dn.
-            attributeToSearch = "memberIdAttr" if memberIdAttr else "dn"
-
-        returnValue(recordsByAlias.values())
-
-
-    def recordTypeForDN(self, dnStr):
-        """
-        Examine a DN to determine which recordType it belongs to
-        @param dn: DN to compare
-        @type dn: string
-        @return: recordType string, or None if no match
-        """
-        dn = ldap.dn.str2dn(dnStr.lower())
-        for recordType in self.recordTypes():
-            base = self.typeDNs[recordType]  # already lowercase
-            if dnContainedIn(dn, base):
-                return recordType
-        return None
-
-
-
-def dnContainedIn(child, parent):
-    """
-    Return True if child dn is contained within parent dn, otherwise False.
-    """
-    return child[-len(parent):] == parent
-
-
-
-def normalizeDNstr(dnStr):
-    """
-    Convert to lowercase and remove extra whitespace
-    @param dnStr: dn
-    @type dnStr: C{str}
-    @return: normalized dn C{str}
-    """
-    return ' '.join(ldap.dn.dn2str(ldap.dn.str2dn(dnStr.lower())).split())
-
-
-
-def _convertValue(value, matchType):
-    if matchType == "starts-with":
-        value = "%s*" % (ldapEsc(value),)
-    elif matchType == "contains":
-        value = "*%s*" % (ldapEsc(value),)
-    # otherwise it's an exact match
-    else:
-        value = ldapEsc(value)
-    return value
-
-
-
-def buildFilter(recordType, mapping, fields, operand="or", optimizeMultiName=False):
-    """
-    Create an LDAP filter string from a list of tuples representing directory
-    attributes to search
-
-    mapping is a dict mapping internal directory attribute names to ldap names.
-    fields is a list of tuples...
-        (directory field name, value to search, caseless (ignored), matchType)
-    ...where matchType is one of "starts-with", "contains", "exact"
-    """
-
-    converted = []
-    combined = {}
-    for field, value, caseless, matchType in fields:
-        ldapField = mapping.get(field, None)
-        if ldapField:
-            combined.setdefault(field, []).append((value, caseless, matchType))
-            value = _convertValue(value, matchType)
-            if isinstance(ldapField, str):
-                converted.append("(%s=%s)" % (ldapField, value))
-            else:
-                subConverted = []
-                for lf in ldapField:
-                    subConverted.append("(%s=%s)" % (lf, value))
-                converted.append("(|%s)" % "".join(subConverted))
-
-    if len(converted) == 0:
-        return None
-
-    if optimizeMultiName and recordType in ("users", "groups"):
-        for field in [key for key in combined.keys() if key != "guid"]:
-            if len(combined.get(field, [])) > 1:
-                # Client is searching on more than one name -- interpret this as the user
-                # explicitly looking up a user by name (ignoring other record types), and
-                # try the various firstName/lastName permutations:
-                if recordType == "users":
-                    converted = []
-                    for firstName, _ignore_firstCaseless, firstMatchType in combined["firstName"]:
-                        for lastName, _ignore_lastCaseless, lastMatchType in combined["lastName"]:
-                            if firstName != lastName:
-                                firstValue = _convertValue(firstName, firstMatchType)
-                                lastValue = _convertValue(lastName, lastMatchType)
-                                converted.append(
-                                    "(&(%s=%s)(%s=%s))" %
-                                    (mapping["firstName"], firstValue,
-                                     mapping["lastName"], lastValue)
-                                )
-                else:
-                    return None
-
-    if len(converted) == 1:
-        filterstr = converted[0]
-    else:
-        operand = ("|" if operand == "or" else "&")
-        filterstr = "(%s%s)" % (operand, "".join(converted))
-
-    if filterstr:
-        # To reduce the amount of records returned, filter out the ones
-        # that don't have (possibly) required attribute values (record
-        # name, guid)
-        additional = []
-        for key in ("recordName", "guid"):
-            if key in mapping:
-                additional.append("(%s=*)" % (mapping.get(key),))
-        if additional:
-            filterstr = "(&%s%s)" % ("".join(additional), filterstr)
-
-    return filterstr
-
-
-
-def buildFilterFromTokens(recordType, mapping, tokens, extra=None):
-    """
-    Create an LDAP filter string from a list of query tokens.  Each token is
-    searched for in each LDAP attribute corresponding to "fullName" and
-    "emailAddresses" (could be multiple LDAP fields for either).
-
-    @param recordType: The recordType to use to customize the filter
-    @param mapping: A dict mapping internal directory attribute names to ldap names.
-    @type mapping: C{dict}
-    @param tokens: The list of tokens to search for
-    @type tokens: C{list}
-    @param extra: Extra filter to "and" into the final filter
-    @type extra: C{str} or None
-    @return: An LDAP filterstr
-    @rtype: C{str}
-    """
-
-    filterStr = None
-
-    # Eliminate any substring duplicates
-    tokenSet = set()
-    for token in tokens:
-        collision = False
-        for existing in tokenSet:
-            if token in existing:
-                collision = True
-                break
-            elif existing in token:
-                tokenSet.remove(existing)
-                break
-        if not collision:
-            tokenSet.add(token)
-
-    tokens = [ldapEsc(t) for t in tokenSet]
-    if len(tokens) == 0:
-        return None
-    tokens.sort()
-
-    attributes = [
-        ("fullName", "(%s=*%s*)"),
-        ("emailAddresses", "(%s=%s*)"),
-    ]
-
-    ldapFields = []
-    for attribute, template in attributes:
-        ldapField = mapping.get(attribute, None)
-        if ldapField:
-            if isinstance(ldapField, str):
-                ldapFields.append((ldapField, template))
-            else:
-                for lf in ldapField:
-                    ldapFields.append((lf, template))
-
-    if len(ldapFields) == 0:
-        return None
-
-    tokenFragments = []
-    if extra:
-        tokenFragments.append(extra)
-
-    for token in tokens:
-        fragments = []
-        for ldapField, template in ldapFields:
-            fragments.append(template % (ldapField, token))
-        if len(fragments) == 1:
-            tokenFragment = fragments[0]
-        else:
-            tokenFragment = "(|%s)" % ("".join(fragments),)
-        tokenFragments.append(tokenFragment)
-
-    if len(tokenFragments) == 1:
-        filterStr = tokenFragments[0]
-    else:
-        filterStr = "(&%s)" % ("".join(tokenFragments),)
-
-    return filterStr
-
-
-
-class LdapDirectoryRecord(CachingDirectoryRecord):
-    """
-    LDAP implementation of L{IDirectoryRecord}.
-    """
-    def __init__(
-        self, service, recordType,
-        guid, shortNames, authIDs, fullName,
-        firstName, lastName, emailAddresses,
-        uid, dn, memberGUIDs, extProxies, extReadOnlyProxies,
-        attrs, **kwargs
-    ):
-        super(LdapDirectoryRecord, self).__init__(
-            service=service,
-            recordType=recordType,
-            guid=guid,
-            shortNames=shortNames,
-            authIDs=authIDs,
-            fullName=fullName,
-            firstName=firstName,
-            lastName=lastName,
-            emailAddresses=emailAddresses,
-            extProxies=extProxies,
-            extReadOnlyProxies=extReadOnlyProxies,
-            uid=uid,
-            **kwargs
-        )
-
-        # Save attributes of dn and attrs in case you might need them later
-        self.dn = dn
-        self.attrs = attrs
-
-        # Store copy of member guids
-        self._memberGUIDs = memberGUIDs
-
-        # Identifier of this record as a group member
-        memberIdAttr = self.service.groupSchema["memberIdAttr"]
-        if memberIdAttr:
-            self._memberId = self.service._getUniqueLdapAttribute(
-                attrs,
-                memberIdAttr
-            )
-        else:
-            self._memberId = normalizeDNstr(self.dn)
-
-
-    def members(self):
-        """ Return the records representing members of this group """
-
-        try:
-            return self._members_storage
-        except AttributeError:
-            self._members_storage = self._members()
-            return self._members_storage
-
-
-    def _members(self):
-        """ Fault in records for the members of this group """
-
-        memberIdAttr = self.service.groupSchema["memberIdAttr"]
-        results = []
-
-        for memberId in self._memberGUIDs:
-
-            if memberIdAttr:
-
-                base = self.service.base
-                filterstr = "(%s=%s)" % (memberIdAttr, ldapEsc(memberId))
-                self.log.debug(
-                    "Retrieving subtree of {base} with filter {filter}",
-                    base=ldap.dn.dn2str(base), filter=filterstr,
-                    system="LdapDirectoryService"
-                )
-                result = self.service.timedSearch(
-                    ldap.dn.dn2str(base),
-                    ldap.SCOPE_SUBTREE,
-                    filterstr=filterstr,
-                    attrlist=self.service.attrlist
-                )
-
-            else:  # using DN
-
-                self.log.debug(
-                    "Retrieving {id}.",
-                    id=memberId, system="LdapDirectoryService"
-                )
-                result = self.service.timedSearch(
-                    memberId,
-                    ldap.SCOPE_BASE, attrlist=self.service.attrlist
-                )
-
-            if result:
-
-                dn, attrs = result.pop()
-                dn = normalizeDNstr(dn)
-                self.log.debug("Retrieved: {dn} {attrs}", dn=dn, attrs=attrs)
-                recordType = self.service.recordTypeForDN(dn)
-                if recordType is None:
-                    self.log.error(
-                        "Unable to map {dn} to a record type", dn=dn
-                    )
-                    continue
-
-                shortName = self.service._getUniqueLdapAttribute(
-                    attrs,
-                    self.service.rdnSchema[recordType]["mapping"]["recordName"]
-                )
-
-                if shortName:
-                    record = self.service.recordWithShortName(
-                        recordType,
-                        shortName
-                    )
-                    if record:
-                        results.append(record)
-
-        return results
-
-
-    def groups(self):
-        """ Return the records representing groups this record is a member of """
-        try:
-            return self._groups_storage
-        except AttributeError:
-            self._groups_storage = self._groups()
-            return self._groups_storage
-
-
-    def _groups(self):
-        """ Fault in the groups of which this record is a member """
-
-        recordType = self.service.recordType_groups
-        base = self.service.typeDNs[recordType]
-
-        membersAttrs = []
-        if self.service.groupSchema["membersAttr"]:
-            membersAttrs.append(self.service.groupSchema["membersAttr"])
-        if self.service.groupSchema["nestedGroupsAttr"]:
-            membersAttrs.append(self.service.groupSchema["nestedGroupsAttr"])
-
-        if len(membersAttrs) == 1:
-            filterstr = "(%s=%s)" % (membersAttrs[0], self._memberId)
-        else:
-            filterstr = "(|%s)" % (
-                "".join(
-                    ["(%s=%s)" % (a, self._memberId) for a in membersAttrs]
-                ),
-            )
-        self.log.debug("Finding groups containing {id}", id=self._memberId)
-        groups = []
-
-        try:
-            results = self.service.timedSearch(
-                ldap.dn.dn2str(base),
-                ldap.SCOPE_SUBTREE,
-                filterstr=filterstr,
-                attrlist=self.service.attrlist
-            )
-
-            for dn, attrs in results:
-                dn = normalizeDNstr(dn)
-                shortName = self.service._getUniqueLdapAttribute(attrs, "cn")
-                self.log.debug(
-                    "{id} is a member of {shortName}",
-                    id=self._memberId, shortName=shortName
-                )
-                record = self.service.recordWithShortName(recordType, shortName)
-                if record is not None:
-                    groups.append(record)
-        except ldap.PROTOCOL_ERROR, e:
-            self.log.warn("{e}", e=e)
-
-        return groups
-
-
-    def cachedGroupsAlias(self):
-        """
-        See directory.py for full description
-
-        LDAP group members can be referred to by attributes other than guid.  _memberId
-        will be set to the appropriate value to look up group-membership with.
-        """
-        return self._memberId
-
-
-    def memberGUIDs(self):
-        return set(self._memberGUIDs)
-
-
-    def verifyCredentials(self, credentials):
-        """ Supports PAM or simple LDAP bind for username+password """
-
-        if isinstance(credentials, UsernamePassword):
-
-            # TODO: investigate:
-            # Check that the username supplied matches one of the shortNames
-            # (The DCS might already enforce this constraint, not sure)
-            if credentials.username not in self.shortNames:
-                return False
-
-            # Check cached password
-            try:
-                if credentials.password == self.password:
-                    return True
-            except AttributeError:
-                pass
-
-            if self.service.authMethod.upper() == "PAM":
-                # Authenticate against PAM (UNTESTED)
-
-                if not pamAvailable:
-                    self.log.error("PAM module is not installed")
-                    raise DirectoryConfigurationError()
-
-                def pam_conv(auth, query_list, userData):
-                    return [(credentials.password, 0)]
-
-                auth = PAM.pam()
-                auth.start("caldav")
-                auth.set_item(PAM.PAM_USER, credentials.username)
-                auth.set_item(PAM.PAM_CONV, pam_conv)
-                try:
-                    auth.authenticate()
-                except PAM.error:
-                    return False
-                else:
-                    # Cache the password to avoid further LDAP queries
-                    self.password = credentials.password
-                    return True
-
-            elif self.service.authMethod.upper() == "LDAP":
-
-                # Authenticate against LDAP
-                try:
-                    self.service.authenticate(self.dn, credentials.password)
-                    # Cache the password to avoid further LDAP queries
-                    self.password = credentials.password
-                    return True
-
-                except ldap.INVALID_CREDENTIALS:
-                    self.log.info(
-                        "Invalid credentials for {dn}",
-                        dn=repr(self.dn), system="LdapDirectoryService"
-                    )
-                    return False
-
-            else:
-                self.log.error(
-                    "Unknown Authentication Method {method!r}",
-                    method=self.service.authMethod.upper()
-                )
-                raise DirectoryConfigurationError()
-
-        return super(LdapDirectoryRecord, self).verifyCredentials(credentials)
-
-
-
-class MissingRecordNameException(Exception):
-    """ Raised when LDAP record is missing recordName """
-    pass
-
-
-
-class MissingGuidException(Exception):
-    """ Raised when LDAP record is missing guidAttr and it's required """
-    pass

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/opendirectorybacker.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/opendirectorybacker.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/opendirectorybacker.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,1960 +0,0 @@
-##
-# Copyright (c) 2006-2014 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.
-##
-
-
-"""
-Apple Open Directory directory service implementation for backing up directory-backed address books
-"""
-
-__all__ = [
-    "OpenDirectoryBackingService", "VCardRecord",
-]
-
-from calendarserver.platform.darwin.od import opendirectory, dsattributes, dsquery
-
-from pycalendar.datetime import DateTime
-from pycalendar.vcard.adr import Adr
-from pycalendar.vcard.n import N
-
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from txweb2.dav.resource import DAVPropertyMixIn
-from txweb2.dav.util import joinURL
-from txweb2.http_headers import MimeType, generateContentType, ETag
-
-from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue, deferredGenerator, succeed
-
-from twistedcaldav import customxml, carddavxml
-from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.vcard import Component, Property, vCardProductID
-
-from txdav.carddav.datastore.query.filter import IsNotDefined, ParameterFilter, \
-    TextMatch
-from txdav.xml import element as davxml
-from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, twisted_private_namespace
-
-from os import listdir
-from os.path import join, abspath
-from random import random
-from socket import getfqdn
-from tempfile import mkstemp, gettempdir
-from xmlrpclib import datetime
-import hashlib
-import os
-import sys
-import time
-import traceback
-
-class OpenDirectoryBackingService(DirectoryService):
-    """
-    Open Directory implementation of L{IDirectoryService}.
-    """
-
-    baseGUID = "BF07A1A2-5BB5-4A4D-A59A-67260EA7E143"
-
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.realmName,)
-
-
-    def __init__(self, params):
-        self._actuallyConfigure(**params)
-
-
-    def _actuallyConfigure(
-        self, queryPeopleRecords=True,
-        peopleNode="/Search/Contacts",
-        queryUserRecords=True,
-        userNode="/Search",
-        maxDSQueryRecords=0,            # maximum number of records requested for any ds query
-
-        queryDSLocal=False,             # query in DSLocal -- debug
-        dsLocalCacheTimeout=30,
-        ignoreSystemRecords=True,
-
-        liveQuery=True,                 # query directory service as needed
-        fakeETag=True,                  # eTag is not reliable if True
-
-        cacheQuery=False,
-        cacheTimeout=30,                # cache timeout
-
-        addDSAttrXProperties=False,        # add dsattributes to vcards as "X-" attributes
-        standardizeSyntheticUIDs=False,  # use simple synthetic UIDs --- good for testing
-        appleInternalServer=False,
-
-        additionalAttributes=[],
-        allowedAttributes=[],
-        directoryBackedAddressBook=None
-    ):
-        """
-        @queryPeopleRecords: C{True} to query for People records
-        @queryUserRecords: C{True} to query for User records
-        @maxDSQueryRecords: maximum number of (unfiltered) ds records retrieved before raising
-            NumberOfMatchesWithinLimits exception or returning results
-        @dsLocalCacheTimeout: how log to keep cache of DSLocal records
-        @liveQuery: C{True} to query the directory as needed
-        @fakeETag: C{True} to use a fake eTag; allows ds queries with partial attributes
-        @cacheQuery: C{True} to query the directory and cache results
-        @cacheTimeout: if caching, the average cache timeout
-        @standardizeSyntheticUIDs: C{True} when creating synthetic UID (==f(Node, Type, Record Name)),
-            use a standard Node name. This allows testing with the same UID on different hosts
-        @allowedAttributes: list of DSAttributes that are used to create VCards
-
-        """
-        assert directoryBackedAddressBook is not None
-        self.directoryBackedAddressBook = directoryBackedAddressBook
-
-        self.peopleDirectory = None
-        self.peopleNode = None
-        self.userDirectory = None
-        self.userNode = None
-
-        self.realmName = None # needed for super
-
-        if queryPeopleRecords or not queryUserRecords:
-            self.peopleNode = peopleNode
-            try:
-                self.peopleDirectory = opendirectory.odInit(peopleNode)
-            except opendirectory.ODError, e:
-                self.log.error("Open Directory (node=%s) Initialization error: %s" % (peopleNode, e))
-                raise
-            self.realmName = peopleNode
-
-        if queryUserRecords:
-            if self.peopleNode == userNode:          # use sane directory and node if they are equal
-                self.userNode = self.peopleNode
-                self.userDirectory = self.peopleDirectory
-            else:
-                self.userNode = userNode
-                try:
-                    self.userDirectory = opendirectory.odInit(userNode)
-                except opendirectory.ODError, e:
-                    self.log.error("Open Directory (node=%s) Initialization error: %s" % (userNode, e))
-                    raise
-                if self.realmName:
-                    self.realmName += "+" + userNode
-                else:
-                    self.realmName = userNode
-
-        self.maxDSQueryRecords = maxDSQueryRecords
-
-        self.ignoreSystemRecords = ignoreSystemRecords
-        self.queryDSLocal = queryDSLocal
-        self.dsLocalCacheTimeout = dsLocalCacheTimeout
-
-        self.liveQuery = liveQuery or not cacheQuery
-        self.fakeETag = fakeETag
-
-        self.cacheQuery = cacheQuery
-
-        self.cacheTimeout = cacheTimeout if cacheTimeout > 0 else 30
-
-        self.addDSAttrXProperties = addDSAttrXProperties
-        self.standardizeSyntheticUIDs = standardizeSyntheticUIDs
-        self.appleInternalServer = appleInternalServer
-
-        self.additionalAttributes = additionalAttributes
-        # filter allows attributes, but make sure there are a minimum of attributes for functionality
-        if allowedAttributes:
-            self.allowedDSQueryAttributes = sorted(list(set(
-                                                [attr for attr in VCardRecord.allDSQueryAttributes
-                                                    if (isinstance(attr, str) and attr in allowedAttributes) or
-                                                       (isinstance(attr, tuple) and attr[0] in allowedAttributes)] +
-                                                VCardRecord.dsqueryAttributesForProperty.get("X-INTERNAL-REQUIRED")
-                                                )))
-            if (self.allowedDSQueryAttributes != VCardRecord.allDSQueryAttributes):
-                self.log.info("Allowed DS query attributes = %r" % (self.allowedDSQueryAttributes,))
-        else:
-            self.allowedDSQueryAttributes = VCardRecord.allDSQueryAttributes
-
-        #self.returnedAttributes = VCardRecord.allDSQueryAttributes
-        self.returnedAttributes = self.allowedDSQueryAttributes
-
-        self._dsLocalRecords = []
-        self._nextDSLocalQueryTime = 0
-
-        # get this now once
-        hostname = getfqdn()
-        if hostname:
-            self.defaultNodeName = "/LDAPv3/" + hostname
-        else:
-            self.defaultNodeName = None
-
-        #cleanup
-        self._cleanupTime = time.time()
-
-        # file system locks
-        self._initLockPath = join(config.DocumentRoot, ".directory_address_book_create_lock")
-        self._createdLockPath = join(config.DocumentRoot, ".directory_address_book_created_lock")
-        self._updateLockPath = join(config.DocumentRoot, ".directory_address_book_update_lock")
-        self._tmpDirAddressBookLockPath = join(config.DocumentRoot, ".directory_address_book_tmpFolder_lock")
-
-        self._updateLock = MemcacheLock("OpenDirectoryBacker", self._updateLockPath)
-        self._tmpDirAddressBookLock = MemcacheLock("OpenDirectoryBacker", self._tmpDirAddressBookLockPath)
-
-        # optimization so we don't have to always get create lock
-        self._triedCreateLock = False
-        self._created = False
-
-
-    def __cmp__(self, other):
-        if not isinstance(other, DirectoryRecord):
-            return super(DirectoryRecord, self).__eq__(other)
-
-        for attr in ("directory", "node"):
-            diff = cmp(getattr(self, attr), getattr(other, attr))
-            if diff != 0:
-                return diff
-        return 0
-
-
-    def __hash__(self):
-        h = hash(self.__class__.__name__)
-        for attr in ("node",):
-            h = (h + hash(getattr(self, attr))) & sys.maxint
-        return h
-
-
-    @inlineCallbacks
-    def available(self):
-        if not self._triedCreateLock:
-            returnValue(False)
-        elif not self._created:
-            createdLock = MemcacheLock("OpenDirectoryBacker", self._createdLockPath)
-            self.log.debug("blocking on lock of: \"%s\")" % self._createdLockPath)
-            self._created = (yield createdLock.locked())
-
-        returnValue(self._created)
-
-
-    def updateLock(self):
-        return self._updateLock
-
-
-    @inlineCallbacks
-    def createCache(self):
-        """
-        If caching, create the cache for the first time.
-        """
-
-        if not self.liveQuery:
-            self.log.info("loading directory address book")
-
-            # get init lock
-            initLock = MemcacheLock("OpenDirectoryBacker", self._initLockPath, timeout=0)
-            self.log.debug("Attempt lock of: \"%s\")" % self._initLockPath)
-            gotCreateLock = False
-            try:
-                yield initLock.acquire()
-                gotCreateLock = True
-            except MemcacheLockTimeoutError:
-                pass
-
-            self._triedCreateLock = True
-
-            if gotCreateLock:
-                self.log.debug("Got lock!")
-                yield self._refreshCache(flushCache=False, creating=True)
-            else:
-                self.log.debug("Could not get lock - directory address book will be filled by peer")
-
-
-    @inlineCallbacks
-    def _refreshCache(self, flushCache=False, creating=False, reschedule=True, query=None, attributes=None, keepLock=False, clear=False, maxRecords=0):
-        """
-        refresh the cache.
-        """
-
-        #print("_refreshCache:, flushCache=%s, creating=%s, reschedule=%s, query = %s" % (flushCache, creating, reschedule, "None" if query is None else query.generate(),))
-
-        def refreshLater():
-            #
-            # Add jitter/fuzz factor to avoid stampede for large OD query
-            #
-            cacheTimeout = min(self.cacheTimeout, 60) * 60
-            cacheTimeout = (cacheTimeout * random()) - (cacheTimeout / 2)
-            cacheTimeout += self.cacheTimeout * 60
-            reactor.callLater(cacheTimeout, self._refreshCache) #@UndefinedVariable
-            self.log.info("Refresh directory address book in %d minutes %d seconds" % divmod(cacheTimeout, 60))
-
-        def cleanupLater():
-
-            # try to cancel previous call if last clean up was less than 15 minutes ago
-            if (time.time() - self._cleanupTime) < 15 * 60:
-                try:
-                    self._lastCleanupCall.cancel()
-                except:
-                    pass
-
-            #
-            # Add jitter/fuzz factor
-            #
-            nom = 120
-            later = nom * (random() + .5)
-            self._lastCleanupCall = reactor.callLater(later, removeTmpAddressBooks) #@UndefinedVariable
-            self.log.info("Remove temporary directory address books in %d minutes %d seconds" % divmod(later, 60))
-
-
-        def getTmpDirAndTmpFilePrefixSuffix():
-            # need to have temp file on same volumes as documents so that move works
-            absDocPath = abspath(config.DocumentRoot)
-            if absDocPath.startswith("/Volumes/"):
-                tmpDir = absDocPath
-                prefix = ".directoryAddressBook-"
-            else:
-                tmpDir = gettempdir()
-                prefix = "directoryAddressBook-"
-
-            return (tmpDir, prefix, ".tmp")
-
-        def makeTmpFilename():
-            tmpDir, prefix, suffix = getTmpDirAndTmpFilePrefixSuffix()
-            fd, fname = mkstemp(suffix=suffix, prefix=prefix, dir=tmpDir)
-            os.close(fd)
-            os.remove(fname)
-            return fname
-
-        @inlineCallbacks
-        def removeTmpAddressBooks():
-            self.log.info("Checking for temporary directory address books")
-            tmpDir, prefix, suffix = getTmpDirAndTmpFilePrefixSuffix()
-
-            tmpDirLock = self._tmpDirAddressBookLock
-            self.log.debug("blocking on lock of: \"%s\")" % self._tmpDirAddressBookLockPath)
-            yield tmpDirLock.acquire()
-
-            try:
-                for name in listdir(tmpDir):
-                    if name.startswith(prefix) and name.endswith(suffix):
-                        try:
-                            path = join(tmpDir, name)
-                            self.log.info("Deleting temporary directory address book at: %s" % path)
-                            FilePath(path).remove()
-                            self.log.debug("Done deleting")
-                        except:
-                            self.log.info("Deletion failed")
-            finally:
-                self.log.debug("unlocking: \"%s\")" % self._tmpDirAddressBookLockPath)
-                yield tmpDirLock.release()
-
-            self._cleanupTime = time.time()
-
-        updateLock = None
-        limited = False
-        try:
-
-            try:
-                # get the records
-                if clear:
-                    records = {}
-                else:
-                    records, limited = (yield self._getDirectoryRecords(query, attributes, maxRecords))
-
-                # calculate the hash
-                # simple for now, could use MD5 digest if too many collisions
-                newAddressBookCTag = customxml.GETCTag(str(hash(self.baseGUID + ":" + self.realmName + ":" + "".join(str(hash(records[key])) for key in records.keys()))))
-
-                # get the old hash
-                oldAddressBookCTag = ""
-                updateLock = self.updateLock()
-                self.log.debug("blocking on lock of: \"%s\")" % self._updateLockPath)
-                yield updateLock.acquire()
-
-                if not flushCache:
-                    # get update lock
-                    try:
-                        oldAddressBookCTag = self.directoryBackedAddressBook.readDeadProperty((calendarserver_namespace, "getctag"))
-                    except:
-                        oldAddressBookCTag = ""
-
-                self.log.debug("Comparing {http://calendarserver.org/ns/}getctag: new = %s, old = %s" % (newAddressBookCTag, oldAddressBookCTag))
-                if str(newAddressBookCTag) != str(oldAddressBookCTag):
-
-                    self.log.debug("unlocking: \"%s\")" % self._updateLockPath)
-                    yield updateLock.release()
-                    updateLock = None
-
-                if not keepLock:
-                    self.log.debug("unlocking: \"%s\")" % self._updateLockPath)
-                    yield updateLock.release()
-                    updateLock = None
-
-            except:
-                cleanupLater()
-                if reschedule:
-                    refreshLater()
-                raise
-
-            if creating:
-                createdLock = MemcacheLock("OpenDirectoryBacker", self._createdLockPath)
-                self.log.debug("blocking on lock of: \"%s\")" % self._createdLockPath)
-                yield createdLock.acquire()
-
-            cleanupLater()
-            if reschedule:
-                refreshLater()
-
-        except:
-            if updateLock:
-                yield updateLock.release()
-            raise
-
-        returnValue((updateLock, limited))
-
-
-    def _getDSLocalRecords(self):
-
-        def generateDSLocalRecords():
-
-            records = {}
-
-            recordTypes = [dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers, ]
-            try:
-                localNodeDirectory = opendirectory.odInit("/Local/Default")
-                self.log.debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r)" % (
-                        "/DSLocal",
-                        recordTypes,
-                        self.returnedAttributes,
-                    ))
-                results = list(opendirectory.listAllRecordsWithAttributes_list(
-                        localNodeDirectory,
-                        recordTypes,
-                        self.returnedAttributes,
-                    ))
-            except opendirectory.ODError, ex:
-                self.log.error("Open Directory (node=%s) error: %s" % ("/Local/Default", str(ex)))
-                raise
-
-            self._dsLocalRecords = []
-            for (recordShortName, value) in results: #@UnusedVariable
-
-                record = VCardRecord(self, value, "/Local/Default")
-
-                if self.ignoreSystemRecords:
-                    # remove system users and people
-                    if record.guid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
-                        self.log.info("Ignoring vcard for system record %s" % (record,))
-                        continue
-
-                if record.guid in records:
-                    self.log.info("Record skipped due to conflict (duplicate uuid): %s" % (record,))
-                else:
-                    try:
-                        vCardText = record.vCardText()
-                    except:
-                        traceback.print_exc()
-                        self.log.info("Could not get vcard for record %s" % (record,))
-                    else:
-                        self.log.debug("VCard text =\n%s" % (vCardText,))
-                        records[record.guid] = record
-
-            return records
-
-        if not self.liveQuery or not self.queryDSLocal:
-            return {}
-
-        if time.time() > self._nextDSLocalQueryTime:
-            self._dsLocalRecords = generateDSLocalRecords()
-            # Add jitter/fuzz factor
-            self._nextDSLocalQueryTime = time.time() + self.dsLocalCacheTimeout * (random() + 0.5) * 60
-
-        return self._dsLocalRecords
-
-
-    @inlineCallbacks
-    def _getDirectoryRecords(self, query=None, attributes=None, maxRecords=0):
-        """
-        Get a list of filtered VCardRecord for the given query with the given attributes.
-        query == None gets all records. attribute == None gets VCardRecord.allDSQueryAttributes
-        """
-        limited = False
-        queryResults = (yield self._queryDirectory(query, attributes, maxRecords))
-        if maxRecords and len(queryResults) >= maxRecords:
-            limited = True
-            self.log.debug("Directory address book record limit (= %d) reached." % (maxRecords,))
-
-        self.log.debug("Query done. Inspecting %s results" % len(queryResults))
-
-        records = self._getDSLocalRecords().copy()
-        self.log.debug("Adding %s DSLocal results" % len(records.keys()))
-
-        for (recordShortName, value) in queryResults: #@UnusedVariable
-
-            record = VCardRecord(self, value, self.defaultNodeName)
-
-            if self.ignoreSystemRecords:
-                # remove system users and people
-                if record.guid.startswith("FFFFEEEE-DDDD-CCCC-BBBB-AAAA"):
-                    self.log.info("Ignoring vcard for system record %s" % (record,))
-                    continue
-
-            if record.guid in records:
-                self.log.info("Ignoring vcard for record due to conflict (duplicate uuid): %s" % (record,))
-            else:
-                records[record.guid] = record
-
-        self.log.debug("After filtering, %s records (limited=%s)." % (len(records), limited))
-        returnValue((records, limited,))
-
-
-    def _queryDirectory(self, query=None, attributes=None, maxRecords=0):
-
-        startTime = time.time()
-
-        if not attributes:
-            attributes = self.returnedAttributes
-
-        attributes = list(set(attributes + self.additionalAttributes)) # remove duplicates
-
-        directoryAndRecordTypes = []
-        if self.peopleDirectory == self.userDirectory:
-            # use single ds query if possible for best performance
-            directoryAndRecordTypes.append((self.peopleDirectory, self.peopleNode, (dsattributes.kDSStdRecordTypePeople, dsattributes.kDSStdRecordTypeUsers)))
-        else:
-            if self.peopleDirectory:
-                directoryAndRecordTypes.append((self.peopleDirectory, self.peopleNode, dsattributes.kDSStdRecordTypePeople))
-            if self.userDirectory:
-                directoryAndRecordTypes.append((self.userDirectory, self.userNode, dsattributes.kDSStdRecordTypeUsers))
-
-        allResults = []
-        for directory, node, recordType in directoryAndRecordTypes:
-            try:
-                if query:
-                    if isinstance(query, dsquery.match) and query.value is not "":
-                        self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r,%r)" % (
-                            node,
-                            query.attribute,
-                            query.value,
-                            query.matchType,
-                            False,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
-                        results = list(
-                            opendirectory.queryRecordsWithAttribute_list(
-                                directory,
-                                query.attribute,
-                                query.value,
-                                query.matchType,
-                                False,
-                                recordType,
-                                attributes,
-                                maxRecords,
-                            ))
-                    else:
-                        self.log.debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r)" % (
-                            node,
-                            query.generate(),
-                            False,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
-                        results = list(
-                            opendirectory.queryRecordsWithAttributes_list(
-                                directory,
-                                query.generate(),
-                                False,
-                                recordType,
-                                attributes,
-                                maxRecords,
-                            ))
-                else:
-                    self.log.debug("opendirectory.listAllRecordsWithAttributes_list(%r,%r,%r,%r)" % (
-                        node,
-                        recordType,
-                        attributes,
-                        maxRecords,
-                    ))
-                    results = list(
-                        opendirectory.listAllRecordsWithAttributes_list(
-                            directory,
-                            recordType,
-                            attributes,
-                            maxRecords,
-                        ))
-            except opendirectory.ODError, ex:
-                self.log.error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
-                raise
-
-            allResults.extend(results)
-
-            if maxRecords:
-                maxRecords -= len(results)
-                if maxRecords <= 0:
-                    break
-
-        elaspedTime = time.time() - startTime
-        self.log.info("Timing: Directory query: %.1f ms (%d records, %.2f records/sec)" % (elaspedTime * 1000, len(allResults), len(allResults) / elaspedTime))
-        return succeed(allResults)
-
-
-    def _getDSFilter(self, addressBookFilter):
-        """
-        Convert the supplied addressbook-query into an expression tree.
-
-        @param filter: the L{Filter} for the addressbook-query to convert.
-        @return: (needsAllRecords, espressionAttributes, expression) tuple
-        """
-        def propFilterListQuery(filterAllOf, propFilters):
-
-            def propFilterExpression(filterAllOf, propFilter):
-                #print("propFilterExpression")
-                """
-                Create an expression for a single prop-filter element.
-
-                @param propFilter: the L{PropertyFilter} element.
-                @return: (needsAllRecords, espressionAttributes, expressions) tuple
-                """
-
-                def definedExpression(defined, allOf, filterName, constant, queryAttributes, allAttrStrings):
-                    if constant or filterName in ("N" , "FN", "UID",):
-                        return (defined, [], [])     # all records have this property so no records do not have it
-                    else:
-                        matchList = list(set([dsquery.match(attrName, "", dsattributes.eDSStartsWith) for attrName in allAttrStrings]))
-                        if defined:
-                            return andOrExpression(allOf, queryAttributes, matchList)
-                        else:
-                            if len(matchList) > 1:
-                                expr = dsquery.expression(dsquery.expression.OR, matchList)
-                            else:
-                                expr = matchList
-                            return (False, queryAttributes, [dsquery.expression(dsquery.expression.NOT, expr), ])
-                    #end isNotDefinedExpression()
-
-
-                def andOrExpression(propFilterAllOf, queryAttributes, matchList):
-                    #print("andOrExpression(propFilterAllOf=%r, queryAttributes%r, matchList%r)" % (propFilterAllOf, queryAttributes, matchList))
-                    if propFilterAllOf and len(matchList):
-                        # add OR expression because parent will AND
-                        return (False, queryAttributes, [dsquery.expression(dsquery.expression.OR, matchList), ])
-                    else:
-                        return (False, queryAttributes, matchList)
-                    #end andOrExpression()
-
-
-                # short circuit parameter filters
-                def supportedParamter(filterName, paramFilters, propFilterAllOf):
-
-                    def supported(paramFilterName, paramFilterDefined, params):
-                        paramFilterName = paramFilterName.upper()
-                        if len(params.keys()) and ((paramFilterName in params.keys()) != paramFilterDefined):
-                            return False
-                        if len(params[paramFilterName]) and str(paramFilter.qualifier).upper() not in params[paramFilterName]:
-                            return False
-                        return True
-                        #end supported()
-
-                    oneSupported = False
-                    for paramFilter in paramFilters:
-                        if filterName == "PHOTO":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {"ENCODING": ["B", ], "TYPE": ["JPEG", ], }):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == "ADR":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {"TYPE": ["WORK", "PREF", "POSTAL", "PARCEL", ], }):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == "LABEL":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {"TYPE": ["POSTAL", "PARCEL", ]}):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == "TEL":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {"TYPE": [], }): # has params derived from ds attributes
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == "EMAIL":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {"TYPE": [], }): # has params derived from ds attributes
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == "URL":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {}):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif filterName == "KEY":
-                            if propFilterAllOf != supported(paramFilter.filter_name, paramFilter.defined, {"ENCODING": ["B", ], "TYPE": ["PGPPUBILICKEY", "USERCERTIFICATE", "USERPKCS12DATA", "USERSMIMECERTIFICATE", ]}):
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-                        elif not filterName.startswith("X-"): #X- IMHandles X-ABRELATEDNAMES excepted, no other params are used
-                            if propFilterAllOf == paramFilter.defined:
-                                return not propFilterAllOf
-                            oneSupported |= propFilterAllOf
-
-                    if propFilterAllOf:
-                        return True
-                    else:
-                        return oneSupported
-                    #end supportedParamter()
-
-
-                def textMatchElementExpression(propFilterAllOf, textMatchElement):
-
-                    # pre process text match strings for ds query
-                    def getMatchStrings(propFilter, matchString):
-
-                        if propFilter.filter_name in ("REV" , "BDAY",):
-                            rawString = matchString
-                            matchString = ""
-                            for c in rawString:
-                                if not c in "TZ-:":
-                                    matchString += c
-                        elif propFilter.filter_name == "GEO":
-                            matchString = ",".join(matchString.split(";"))
-
-                        if propFilter.filter_name in ("N" , "ADR", "ORG",):
-                            # for structured properties, change into multiple strings for ds query
-                            if propFilter.filter_name == "ADR":
-                                #split by newline and comma
-                                rawStrings = ",".join(matchString.split("\n")).split(",")
-                            else:
-                                #split by space
-                                rawStrings = matchString.split(" ")
-
-                            # remove empty strings
-                            matchStrings = []
-                            for oneString in rawStrings:
-                                if len(oneString):
-                                    matchStrings += [oneString, ]
-                            return matchStrings
-
-                        elif len(matchString):
-                            return [matchString, ]
-                        else:
-                            return []
-                        # end getMatchStrings
-
-                    if constant:
-                        # do the match right now!  Return either all or none.
-                        return(textMatchElement.test([constant, ]), [], [])
-                    else:
-
-                        matchStrings = getMatchStrings(propFilter, textMatchElement.text)
-
-                        if not len(matchStrings) or binaryAttrStrs:
-                            # no searching text in binary ds attributes, so change to defined/not defined case
-                            if textMatchElement.negate:
-                                return definedExpression(False, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
-                            # else fall through to attribute exists case below
-                        else:
-
-                            # special case UID's formed from node and record name
-                            if propFilter.filter_name == "UID":
-                                matchString = matchStrings[0]
-                                seperatorIndex = matchString.find(VCardRecord.peopleUIDSeparator)
-                                if seperatorIndex > 1:
-                                    recordNameStart = seperatorIndex + len(VCardRecord.peopleUIDSeparator)
-                                else:
-                                    seperatorIndex = matchString.find(VCardRecord.userUIDSeparator)
-                                    if seperatorIndex > 1:
-                                        recordNameStart = seperatorIndex + len(VCardRecord.userUIDSeparator)
-                                    else:
-                                        recordNameStart = sys.maxint
-
-                                if recordNameStart < len(matchString) - 1:
-                                    try:
-                                        recordNameQualifier = matchString[recordNameStart:].decode("base64").decode("utf8")
-                                    except Exception, e:
-                                        self.log.debug("Could not decode UID string %r in %r: %r" % (matchString[recordNameStart:], matchString, e,))
-                                    else:
-                                        if textMatchElement.negate:
-                                            return (False, queryAttributes,
-                                                    [dsquery.expression(dsquery.expression.NOT, dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact)), ]
-                                                    )
-                                        else:
-                                            return (False, queryAttributes,
-                                                    [dsquery.match(dsattributes.kDSNAttrRecordName, recordNameQualifier, dsattributes.eDSExact), ]
-                                                    )
-
-                            # use match_type where possible depending on property/attribute mapping
-                            # Note that case sensitive negate will not work
-                            #        Should return all records in that case
-                            matchType = dsattributes.eDSContains
-                            if propFilter.filter_name in ("NICKNAME" , "TITLE" , "NOTE" , "UID", "URL", "N", "ADR", "ORG", "REV", "LABEL",):
-                                if textMatchElement.match_type == "equals":
-                                        matchType = dsattributes.eDSExact
-                                elif textMatchElement.match_type == "starts-with":
-                                        matchType = dsattributes.eDSStartsWith
-                                elif textMatchElement.match_type == "ends-with":
-                                        matchType = dsattributes.eDSEndsWith
-
-                            matchList = []
-                            for matchString in matchStrings:
-                                matchList += [dsquery.match(attrName, matchString, matchType) for attrName in stringAttrStrs]
-
-                            matchList = list(set(matchList))
-
-                            if textMatchElement.negate:
-                                if len(matchList) > 1:
-                                    expr = dsquery.expression(dsquery.expression.OR, matchList)
-                                else:
-                                    expr = matchList
-                                return (False, queryAttributes, [dsquery.expression(dsquery.expression.NOT, expr), ])
-                            else:
-                                return andOrExpression(propFilterAllOf, queryAttributes, matchList)
-
-                    # attribute exists search
-                    return definedExpression(True, propFilterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
-                    #end textMatchElementExpression()
-
-                # get attribute strings from dsqueryAttributesForProperty list
-                queryAttributes = list(set(VCardRecord.dsqueryAttributesForProperty.get(propFilter.filter_name, [])).intersection(set(self.allowedDSQueryAttributes)))
-
-                binaryAttrStrs = []
-                stringAttrStrs = []
-                for attr in queryAttributes:
-                    if isinstance(attr, tuple):
-                        binaryAttrStrs.append(attr[0])
-                    else:
-                        stringAttrStrs.append(attr)
-                allAttrStrings = stringAttrStrs + binaryAttrStrs
-
-                constant = VCardRecord.constantProperties.get(propFilter.filter_name)
-                if not constant and not allAttrStrings:
-                    return (False, [], [])
-
-                if propFilter.qualifier and isinstance(propFilter.qualifier, IsNotDefined):
-                    return definedExpression(False, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
-
-                paramFilterElements = [paramFilterElement for paramFilterElement in propFilter.filters if isinstance(paramFilterElement, ParameterFilter)]
-                textMatchElements = [textMatchElement for textMatchElement in propFilter.filters if isinstance(textMatchElement, TextMatch)]
-                propFilterAllOf = propFilter.propfilter_test == "allof"
-
-                # handle parameter filter elements
-                if len(paramFilterElements) > 0:
-                    if supportedParamter(propFilter.filter_name, paramFilterElements, propFilterAllOf):
-                        if len(textMatchElements) == 0:
-                            return definedExpression(True, filterAllOf, propFilter.filter_name, constant, queryAttributes, allAttrStrings)
-                    else:
-                        if propFilterAllOf:
-                            return (False, [], [])
-
-                # handle text match elements
-                propFilterNeedsAllRecords = propFilterAllOf
-                propFilterAttributes = []
-                propFilterExpressionList = []
-                for textMatchElement in textMatchElements:
-
-                    textMatchNeedsAllRecords, textMatchExpressionAttributes, textMatchExpression = textMatchElementExpression(propFilterAllOf, textMatchElement)
-                    if propFilterAllOf:
-                        propFilterNeedsAllRecords &= textMatchNeedsAllRecords
-                    else:
-                        propFilterNeedsAllRecords |= textMatchNeedsAllRecords
-                    propFilterAttributes += textMatchExpressionAttributes
-                    propFilterExpressionList += textMatchExpression
-
-                if (len(propFilterExpressionList) > 1) and (filterAllOf != propFilterAllOf):
-                    propFilterExpressions = [dsquery.expression(dsquery.expression.AND if propFilterAllOf else dsquery.expression.OR , list(set(propFilterExpressionList)))] # remove duplicates
-                else:
-                    propFilterExpressions = list(set(propFilterExpressionList))
-
-                return (propFilterNeedsAllRecords, propFilterAttributes, propFilterExpressions)
-                #end propFilterExpression
-
-            #print("propFilterListQuery: filterAllOf=%r, propFilters=%r" % (filterAllOf, propFilters,))
-            """
-            Create an expression for a list of prop-filter elements.
-
-            @param filterAllOf: the C{True} if parent filter test is "allof"
-            @param propFilters: the C{list} of L{ComponentFilter} elements.
-            @return: (needsAllRecords, espressionAttributes, expression) tuple
-            """
-            needsAllRecords = filterAllOf
-            attributes = []
-            expressions = []
-            for propFilter in propFilters:
-
-                propNeedsAllRecords, propExpressionAttributes, propExpression = propFilterExpression(filterAllOf, propFilter)
-                if filterAllOf:
-                    needsAllRecords &= propNeedsAllRecords
-                else:
-                    needsAllRecords |= propNeedsAllRecords
-                attributes += propExpressionAttributes
-                expressions += propExpression
-
-            if len(expressions) > 1:
-                expr = dsquery.expression(dsquery.expression.AND if filterAllOf else dsquery.expression.OR , list(set(expressions))) # remove duplicates
-            elif len(expressions):
-                expr = expressions[0]
-            else:
-                expr = None
-
-            return (needsAllRecords, attributes, expr)
-
-        #print("_getDSFilter")
-        # Lets assume we have a valid filter from the outset
-
-        # Top-level filter contains zero or more prop-filters
-        if addressBookFilter:
-            filterAllOf = addressBookFilter.filter_test == "allof"
-            if len(addressBookFilter.children) > 0:
-                return propFilterListQuery(filterAllOf, addressBookFilter.children)
-            else:
-                return (filterAllOf, [], [])
-        else:
-            return (False, [], [])
-
-
-    def _attributesForAddressBookQuery(self, addressBookQuery):
-
-        propertyNames = []
-        #print( "addressBookQuery.qname=%r" % addressBookQuery.qname)
-        if addressBookQuery.qname() == ("DAV:", "prop"):
-
-            for property in addressBookQuery.children:
-                #print("property = %r" % property )
-                if isinstance(property, carddavxml.AddressData):
-                    for addressProperty in property.children:
-                        #print("addressProperty = %r" % addressProperty )
-                        if isinstance(addressProperty, carddavxml.Property):
-                            #print("Adding property %r", addressProperty.attributes["name"])
-                            propertyNames.append(addressProperty.attributes["name"])
-
-                elif not self.fakeETag and property.qname() == ("DAV:", "getetag"):
-                    # for a real etag == md5(vCard), we need all attributes
-                    propertyNames = None
-                    break
-
-        if not len(propertyNames):
-            #print("using all attributes")
-            return self.returnedAttributes
-
-        else:
-            propertyNames.append("X-INTERNAL-MINIMUM-VCARD-PROPERTIES") # these properties are required to make a vCard
-            queryAttributes = []
-            for prop in propertyNames:
-                if prop in VCardRecord.dsqueryAttributesForProperty:
-                    #print("adding attributes %r" % VCardRecord.dsqueryAttributesForProperty.get(prop))
-                    queryAttributes += VCardRecord.dsqueryAttributesForProperty.get(prop)
-
-            return list(set(queryAttributes).intersection(set(self.returnedAttributes)))
-
-
-    @inlineCallbacks
-    def cacheVCardsForAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults):
-        """
-        Cache the vCards for a given addressBookFilder and addressBookQuery
-        """
-        startTime = time.time()
-        #print("Timing: cacheVCardsForAddressBookQuery.starttime=%f" % startTime)
-
-        allRecords, filterAttributes, dsFilter = self._getDSFilter(addressBookFilter)
-        #print("allRecords = %s, query = %s" % (allRecords, "None" if dsFilter is None else dsFilter.generate(),))
-
-        if allRecords:
-            dsFilter = None #  None expression == all Records
-        clear = not allRecords and not dsFilter
-
-        #get unique list of requested attributes
-        if clear:
-            attributes = None
-        else:
-            queryAttributes = self._attributesForAddressBookQuery(addressBookQuery)
-            attributes = filterAttributes + queryAttributes
-
-        #calc maxRecords from passed in maxResults allowing extra for second stage filtering in caller
-        maxRecords = int(maxResults * 1.2)
-        if self.maxDSQueryRecords and maxRecords > self.maxDSQueryRecords:
-            maxRecords = self.maxDSQueryRecords
-
-        updateLock, limited = (yield self._refreshCache(reschedule=False, query=dsFilter, attributes=attributes, keepLock=True, clear=clear, maxRecords=maxRecords))
-
-        elaspedTime = time.time() - startTime
-        self.log.info("Timing: Cache fill: %.1f ms" % (elaspedTime * 1000,))
-
-        returnValue((updateLock, limited))
-
-
-    @inlineCallbacks
-    def vCardRecordsForAddressBookQuery(self, addressBookFilter, addressBookQuery, maxResults):
-        """
-        Get vCards for a given addressBookFilder and addressBookQuery
-        """
-
-        allRecords, filterAttributes, dsFilter = self._getDSFilter(addressBookFilter)
-        #print("allRecords = %s, query = %s" % (allRecords, "None" if dsFilter is None else dsFilter.generate(),))
-
-        # testing:
-        # allRecords = True
-
-        if allRecords:
-            dsFilter = None #  None expression == all Records
-        clear = not allRecords and not dsFilter
-
-        queryRecords = []
-        limited = False
-
-        if not clear:
-            queryAttributes = self._attributesForAddressBookQuery(addressBookQuery)
-            attributes = filterAttributes + queryAttributes
-
-            #calc maxRecords from passed in maxResults allowing extra for second stage filtering in caller
-            maxRecords = int(maxResults * 1.2)
-            if self.maxDSQueryRecords and maxRecords > self.maxDSQueryRecords:
-                maxRecords = self.maxDSQueryRecords
-
-            records, limited = (yield self._getDirectoryRecords(dsFilter, attributes, maxRecords))
-
-            #filter out bad records --- should only happen during development
-            for record in records.values():
-                try:
-                    vCardText = record.vCardText()
-                except:
-                    traceback.print_exc()
-                    self.log.info("Could not get vcard for record %s" % (record,))
-                else:
-                    if not record.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation).startswith("/Local"):
-                        self.log.debug("VCard text =\n%s" % (vCardText,))
-                    queryRecords.append(record)
-
-        returnValue((queryRecords, limited,))
-
-
-
-class VCardRecord(DirectoryRecord, DAVPropertyMixIn):
-    """
-    Open Directory implementation of L{IDirectoryRecord}.
-    """
-
-    # od attributes that may contribute to vcard properties
-    # will be used to translate vCard queries to od queries
-
-    dsqueryAttributesForProperty = {
-
-        "FN" : [
-               dsattributes.kDS1AttrFirstName,
-               dsattributes.kDS1AttrLastName,
-               dsattributes.kDS1AttrMiddleName,
-               dsattributes.kDSNAttrNamePrefix,
-               dsattributes.kDSNAttrNameSuffix,
-               dsattributes.kDS1AttrDistinguishedName,
-               dsattributes.kDSNAttrRecordName,
-               ],
-        "N" : [
-               dsattributes.kDS1AttrFirstName,
-               dsattributes.kDS1AttrLastName,
-               dsattributes.kDS1AttrMiddleName,
-               dsattributes.kDSNAttrNamePrefix,
-               dsattributes.kDSNAttrNameSuffix,
-               dsattributes.kDS1AttrDistinguishedName,
-               dsattributes.kDSNAttrRecordName,
-               ],
-        "NICKNAME" : [
-                dsattributes.kDSNAttrNickName,
-                ],
-        # no binary searching
-        "PHOTO" : [
-                (dsattributes.kDSNAttrJPEGPhoto, "base64"),
-                ],
-        "BDAY" : [
-                dsattributes.kDS1AttrBirthday,
-                ],
-        "ADR" : [
-                dsattributes.kDSNAttrBuilding,
-                dsattributes.kDSNAttrStreet,
-                dsattributes.kDSNAttrCity,
-                dsattributes.kDSNAttrState,
-                dsattributes.kDSNAttrPostalCode,
-                dsattributes.kDSNAttrCountry,
-                ],
-        "LABEL" : [
-                dsattributes.kDSNAttrPostalAddress,
-                dsattributes.kDSNAttrPostalAddressContacts,
-                dsattributes.kDSNAttrAddressLine1,
-                dsattributes.kDSNAttrAddressLine2,
-                dsattributes.kDSNAttrAddressLine3,
-                ],
-         "TEL" : [
-                dsattributes.kDSNAttrPhoneNumber,
-                dsattributes.kDSNAttrMobileNumber,
-                dsattributes.kDSNAttrPagerNumber,
-                dsattributes.kDSNAttrHomePhoneNumber,
-                dsattributes.kDSNAttrPhoneContacts,
-                dsattributes.kDSNAttrFaxNumber,
-                #dsattributes.kDSNAttrAreaCode,
-                ],
-         "EMAIL" : [
-                dsattributes.kDSNAttrEMailAddress,
-                dsattributes.kDSNAttrEMailContacts,
-                ],
-         "GEO" : [
-                dsattributes.kDSNAttrMapCoordinates,
-                ],
-         "TITLE" : [
-                dsattributes.kDSNAttrJobTitle,
-                ],
-         "ORG" : [
-                dsattributes.kDSNAttrCompany,
-                dsattributes.kDSNAttrOrganizationName,
-                dsattributes.kDSNAttrDepartment,
-                ],
-         "NOTE" : [
-                dsattributes.kDS1AttrComment,
-                dsattributes.kDS1AttrNote,
-                ],
-         "REV" : [
-                dsattributes.kDS1AttrModificationTimestamp,
-                ],
-         "UID" : [
-                dsattributes.kDS1AttrGeneratedUID,
-                # special cased
-                #dsattributes.kDSNAttrMetaNodeLocation,
-                #dsattributes.kDSNAttrRecordName,
-                #dsattributes.kDS1AttrDistinguishedName,
-                ],
-         "URL" : [
-                dsattributes.kDS1AttrWeblogURI,
-                dsattributes.kDSNAttrURL,
-                ],
-         "KEY" : [
-                # check on format, are these all binary?
-                (dsattributes.kDSNAttrPGPPublicKey, "base64"),
-                (dsattributes.kDS1AttrUserCertificate, "base64"),
-                (dsattributes.kDS1AttrUserPKCS12Data, "base64"),
-                (dsattributes.kDS1AttrUserSMIMECertificate, "base64"),
-                ],
-         # too bad this is not one X-Attribute with params.     Would make searching easier
-         "X-AIM" : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         "X-JABBER" : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         "X-MSN" : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         "X-YAHOO" : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         "X-ICQ" : [
-                dsattributes.kDSNAttrIMHandle,
-                ],
-         "X-ABRELATEDNAMES" : [
-                dsattributes.kDSNAttrRelationships,
-                ],
-          "X-INTERNAL-MINIMUM-VCARD-PROPERTIES" : [
-                dsattributes.kDS1AttrGeneratedUID,
-                dsattributes.kDSNAttrMetaNodeLocation,
-                dsattributes.kDS1AttrFirstName,
-                 dsattributes.kDS1AttrLastName,
-                dsattributes.kDS1AttrMiddleName,
-                   dsattributes.kDSNAttrNamePrefix,
-                  dsattributes.kDSNAttrNameSuffix,
-                 dsattributes.kDS1AttrDistinguishedName,
-                dsattributes.kDSNAttrRecordName,
-                dsattributes.kDSNAttrRecordType,
-                dsattributes.kDS1AttrModificationTimestamp,
-                dsattributes.kDS1AttrCreationTimestamp,
-                ],
-          "X-INTERNAL-REQUIRED" : [
-                dsattributes.kDS1AttrGeneratedUID,
-                dsattributes.kDSNAttrMetaNodeLocation,
-                 dsattributes.kDS1AttrDistinguishedName,
-                dsattributes.kDSNAttrRecordName,
-                dsattributes.kDS1AttrFirstName,
-                 dsattributes.kDS1AttrLastName,
-                dsattributes.kDSNAttrRecordType,
-                ],
-
-    }
-
-    allDSQueryAttributes = sorted(list(set([attr for lookupAttributes in dsqueryAttributesForProperty.values()
-                                      for attr in lookupAttributes])))
-
-    binaryDSAttributeStrs = [attr[0] for attr in allDSQueryAttributes
-                                if isinstance(attr, tuple)]
-
-    stringDSAttributeStrs = [attr for attr in allDSQueryAttributes
-                                if isinstance(attr, str)]
-
-    allDSAttributeStrs = stringDSAttributeStrs + binaryDSAttributeStrs
-
-    #peopleUIDSeparator = "-" + OpenDirectoryBackingService.baseGUID + "-"
-    userUIDSeparator = "-bf07a1a2-"
-    peopleUIDSeparator = "-cf07a1a2-"
-
-    constantProperties = {
-        # 3.6.3 PRODID Type Definition
-        "PRODID": vCardProductID,
-        # 3.6.9 VERSION Type Definition
-        "VERSION": "3.0",
-        }
-
-
-    def __init__(self, service, recordAttributes, defaultNodeName=None):
-
-        self.log.debug("service=%s, attributes=%s" % (service, recordAttributes))
-
-        #save off for debugging
-        if service.addDSAttrXProperties:
-            self.originalAttributes = recordAttributes.copy()
-
-        self.directoryBackedAddressBook = service.directoryBackedAddressBook
-        self._vCard = None
-        self._vCardText = None
-        self._uriName = None
-        self._hRef = None
-
-        self.attributes = {}
-        for key, values in recordAttributes.items():
-            if key in VCardRecord.stringDSAttributeStrs:
-                if isinstance(values, list):
-                    self.attributes[key] = [removeControlChars(val).decode("utf8") for val in values]
-                else:
-                    self.attributes[key] = removeControlChars(values).decode("utf8")
-            else:
-                self.attributes[key] = values
-
-        # fill in  missing essential attributes used for filtering
-        fullName = self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)
-        if not fullName:
-            fullName = self.firstValueForAttribute(dsattributes.kDSNAttrRecordName)
-            self.attributes[dsattributes.kDS1AttrDistinguishedName] = fullName
-
-        node = self.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation)
-
-        # use a better node name -- makes better synthetic GUIDS
-        if not node or node == "/LDAPv3/127.0.0.1":
-            node = defaultNodeName if defaultNodeName else service.realmName
-            self.attributes[dsattributes.kDSNAttrMetaNodeLocation] = node
-
-        guid = self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)
-        if not guid:
-            if service.standardizeSyntheticUIDs:
-                nodeUUIDStr = "00000000"
-            else:
-                nodeUUIDStr = "%x" % abs(hash(node))
-            nameUUIDStr = "".join(self.firstValueForAttribute(dsattributes.kDSNAttrRecordName).encode("utf8").encode("base64").split("\n"))
-            if self.firstValueForAttribute(dsattributes.kDSNAttrRecordType) != dsattributes.kDSStdRecordTypePeople:
-                guid = VCardRecord.userUIDSeparator.join([nodeUUIDStr, nameUUIDStr, ])
-            else:
-                guid = VCardRecord.peopleUIDSeparator.join([nodeUUIDStr, nameUUIDStr, ])
-
-        # since guid is used as file name, normalize so uid uniqueness == fine name uniqueness
-        #guid = "/".join(guid.split(":")).upper()
-        self.attributes[dsattributes.kDS1AttrGeneratedUID] = guid
-
-        if self.firstValueForAttribute(dsattributes.kDS1AttrLastName) == "99":
-            del self.attributes[dsattributes.kDS1AttrLastName]
-
-        if self.firstValueForAttribute(dsattributes.kDSNAttrRecordType) != dsattributes.kDSStdRecordTypePeople:
-            recordType = DirectoryService.recordType_users
-        else:
-            recordType = DirectoryService.recordType_people
-
-        super(VCardRecord, self).__init__(
-            service=service,
-            recordType=recordType,
-            guid=guid,
-            shortNames=tuple(self.valuesForAttribute(dsattributes.kDSNAttrRecordName)),
-            fullName=fullName,
-            firstName=self.firstValueForAttribute(dsattributes.kDS1AttrFirstName, None),
-            lastName=self.firstValueForAttribute(dsattributes.kDS1AttrLastName, None),
-            emailAddresses=(),
-            calendarUserAddresses=(),
-            autoSchedule=False,
-            enabledForCalendaring=False,
-        )
-
-
-    def __repr__(self):
-        return "<%s[%s(%s)] %s(%s) %r>" % (
-            self.__class__.__name__,
-            self.firstValueForAttribute(dsattributes.kDSNAttrRecordType),
-            self.firstValueForAttribute(dsattributes.kDSNAttrMetaNodeLocation),
-            self.guid,
-            self.shortNames,
-            self.fullName
-        )
-
-
-    def __hash__(self):
-        s = "".join([
-              "%s:%s" % (attribute, self.valuesForAttribute(attribute),)
-              for attribute in self.attributes
-              ])
-        return hash(s)
-
-    """
-    def nextFileName(self):
-        self.renameCounter += 1
-        self.fileName = self.baseFileName + "-" + str(self.renameCounter)
-        self.fileNameLower = self.fileName.lower()
-    """
-
-    def hasAttribute(self, attributeName):
-        return self.valuesForAttribute(attributeName, None) is not None
-
-
-    def valuesForAttribute(self, attributeName, default_values=[]):
-        values = self.attributes.get(attributeName)
-        if (values is None):
-            return default_values
-        elif not isinstance(values, list):
-            values = [values, ]
-
-        # ds templates often return empty attribute values
-        #     get rid of them here
-        nonEmptyValues = [(value.encode("utf-8") if isinstance(value, unicode) else value) for value in values if len(value) > 0]
-
-        if len(nonEmptyValues) > 0:
-            return nonEmptyValues
-        else:
-            return default_values
-
-
-    def firstValueForAttribute(self, attributeName, default_value=""):
-        values = self.attributes.get(attributeName)
-        if values is None:
-            return default_value
-        elif isinstance(values, list):
-            return values[0].encode("utf_8") if isinstance(values[0], unicode) else values[0]
-        else:
-            return values.encode("utf_8") if isinstance(values, unicode) else values
-
-
-    def joinedValuesForAttribute(self, attributeName, separator=",", default_string=""):
-        values = self.valuesForAttribute(attributeName, None)
-        if not values:
-            return default_string
-        else:
-            return separator.join(values)
-
-
-    def isoDateStringForDateAttribute(self, attributeName, default_string=""):
-        modDate = self.firstValueForAttribute(attributeName, default_string)
-        revDate = None
-        if modDate:
-            if len(modDate) >= len("YYYYMMDD") and modDate[:8].isdigit():
-                revDate = "%s-%s-%s" % (modDate[:4], modDate[4:6], modDate[6:8],)
-            if len(modDate) >= len("YYYYMMDDHHMMSS") and modDate[8:14].isdigit():
-                revDate += "T%s:%s:%sZ" % (modDate[8:10], modDate[10:12], modDate[12:14],)
-        return revDate
-
-
-    def vCard(self):
-
-
-        def generateVCard():
-
-            def isUniqueProperty(vcard, newProperty, ignoreParams=None):
-                existingProperties = vcard.properties(newProperty.name())
-                for existingProperty in existingProperties:
-                    if ignoreParams:
-                        existingProperty = existingProperty.duplicate()
-                        for paramname, paramvalue in ignoreParams:
-                            existingProperty.removeParameterValue(paramname, paramvalue)
-                    if existingProperty == newProperty:
-                        return False
-                return True
-
-            def addUniqueProperty(vcard, newProperty, ignoreParams=None, attrType=None, attrValue=None):
-                if isUniqueProperty(vcard, newProperty, ignoreParams):
-                    vcard.addProperty(newProperty)
-                else:
-                    if attrType and attrValue:
-                        self.log.info("Ignoring attribute %r with value %r in creating property %r. A duplicate property already exists." % (attrType, attrValue, newProperty,))
-
-            def addPropertyAndLabel(groupCount, label, propertyName, propertyValue, parameters=None):
-                groupCount[0] += 1
-                groupPrefix = "item%d" % groupCount[0]
-                vcard.addProperty(Property(propertyName, propertyValue, params=parameters, group=groupPrefix))
-                vcard.addProperty(Property("X-ABLabel", label, group=groupPrefix))
-
-            # for attributes of the form  param:value
-            def addPropertiesAndLabelsForPrefixedAttribute(groupCount, propertyPrefix, propertyName, defaultLabel, nolabelParamTypes, labelMap, attrType):
-                preferred = True
-                for attrValue in self.valuesForAttribute(attrType):
-                    try:
-                        # special case for Apple
-                        if self.service.appleInternalServer and attrType == dsattributes.kDSNAttrIMHandle:
-                            splitValue = attrValue.split("|")
-                            if len(splitValue) > 1:
-                                attrValue = splitValue[0]
-
-                        colonIndex = attrValue.find(":")
-                        if (colonIndex > len(attrValue) - 2):
-                            raise ValueError("Nothing after colon.")
-
-                        propertyValue = attrValue[colonIndex + 1:]
-                        labelString = attrValue[:colonIndex] if colonIndex > 0 else defaultLabel
-                        paramTypeString = labelString.upper()
-
-                        # add PREF to first prop's parameters
-                        paramTypeStrings = [paramTypeString, ]
-                        if preferred and "PREF" != paramTypeString:
-                            paramTypeStrings += ["PREF", ]
-                        parameters = {"TYPE": paramTypeStrings, }
-
-                        #special case for IMHandles which the param is the last part of the property like X-AIM or X-JABBER
-                        if propertyPrefix:
-                            propertyName = propertyPrefix + paramTypeString
-
-                        # only add label prop if needed
-                        if paramTypeString in nolabelParamTypes:
-                            addUniqueProperty(vcard, Property(propertyName, attrValue[colonIndex + 1:], params=parameters), None, attrValue, attrType)
-                        else:
-                            # use special localizable addressbook labels where possible
-                            abLabelString = labelMap.get(labelString, labelString)
-                            addPropertyAndLabel(groupCount, abLabelString, propertyName, propertyValue, parameters)
-                        preferred = False
-
-                    except Exception, e:
-                        traceback.print_exc()
-                        self.log.debug("addPropertiesAndLabelsForPrefixedAttribute(): groupCount=%r, propertyPrefix=%r, propertyName=%r, nolabelParamTypes=%r, labelMap=%r, attrType=%r" % (groupCount[0], propertyPrefix, propertyName, nolabelParamTypes, labelMap, attrType,))
-                        self.log.error("addPropertiesAndLabelsForPrefixedAttribute(): Trouble parsing attribute %s, with value \"%s\".  Error = %s" % (attrType, attrValue, e,))
-
-            #print("VCardRecord.vCard")
-            # create vCard
-            vcard = Component("VCARD")
-            groupCount = [0]
-
-            # add constant properties - properties that are the same regardless of the record attributes
-            for key, value in VCardRecord.constantProperties.items():
-                vcard.addProperty(Property(key, value))
-
-            # 3.1 IDENTIFICATION TYPES http://tools.ietf.org/html/rfc2426#section-3.1
-            # 3.1.1 FN Type Definition
-            # dsattributes.kDS1AttrDistinguishedName,      # Users distinguished or real name
-            #
-            # full name is required but this is set in OpenDiretoryBackingRecord.__init__
-            #vcard.addProperty(Property("FN", self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)))
-
-            # 3.1.2 N Type Definition
-            # dsattributes.kDS1AttrFirstName,            # Used for first name of user or person record.
-            # dsattributes.kDS1AttrLastName,            # Used for the last name of user or person record.
-            # dsattributes.kDS1AttrMiddleName,            # Used for the middle name of user or person record.
-            # dsattributes.kDSNAttrNameSuffix,            # Represents the name suffix of a user or person.
-                                                        #      ie. Jr., Sr., etc.
-                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or
-                                                        #      dsattributes.kDSStdRecordTypePeople).
-            # dsattributes.kDSNAttrNamePrefix,            # Represents the title prefix of a user or person.
-                                                        #      ie. Mr., Ms., Mrs., Dr., etc.
-                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or
-                                                        #      dsattributes.kDSStdRecordTypePeople).
-
-            # name is required, so make sure we have one
-            # vcard says: Each name attribute can be a string or a list of strings.
-            if not self.hasAttribute(dsattributes.kDS1AttrFirstName) and not self.hasAttribute(dsattributes.kDS1AttrLastName):
-                familyName = self.firstValueForAttribute(dsattributes.kDS1AttrDistinguishedName)
-            else:
-                familyName = self.valuesForAttribute(dsattributes.kDS1AttrLastName, "")
-
-            nameObject = N(
-                first=self.valuesForAttribute(dsattributes.kDS1AttrFirstName, ""),
-                last=familyName,
-                middle=self.valuesForAttribute(dsattributes.kDS1AttrMiddleName, ""),
-                prefix=self.valuesForAttribute(dsattributes.kDSNAttrNamePrefix, ""),
-                suffix=self.valuesForAttribute(dsattributes.kDSNAttrNameSuffix, ""),
-            )
-            vcard.addProperty(Property("N", nameObject))
-
-            # set full name to Name with contiguous spaces stripped
-            # it turns out that Address Book.app ignores FN and creates it fresh from N in ABRecord
-            # so no reason to have FN distinct from N
-            vcard.addProperty(Property("FN", nameObject.getFullName()))
-
-            # 3.1.3 NICKNAME Type Definition
-            # dsattributes.kDSNAttrNickName,            # Represents the nickname of a user or person.
-                                                        #    Usually found in user or people records (kDSStdRecordTypeUsers or
-                                                        #    dsattributes.kDSStdRecordTypePeople).
-            for nickname in self.valuesForAttribute(dsattributes.kDSNAttrNickName):
-                addUniqueProperty(vcard, Property("NICKNAME", nickname), None, dsattributes.kDSNAttrNickName, nickname)
-
-            # 3.1.4 PHOTO Type Definition
-            # dsattributes.kDSNAttrJPEGPhoto,            # Used to store binary picture data in JPEG format.
-                                                        #      Usually found in user, people or group records (kDSStdRecordTypeUsers,
-                                                        #      dsattributes.kDSStdRecordTypePeople,dsattributes.kDSStdRecordTypeGroups).
-            # pyOpenDirectory always returns binary-encoded string
-
-            for photo in self.valuesForAttribute(dsattributes.kDSNAttrJPEGPhoto):
-                addUniqueProperty(vcard, Property("PHOTO", photo, params={"ENCODING": ["b", ], "TYPE": ["JPEG", ], }), None, dsattributes.kDSNAttrJPEGPhoto, photo)
-
-            # 3.1.5 BDAY Type Definition
-            # dsattributes.kDS1AttrBirthday,            # Single-valued attribute that defines the user's birthday.
-                                                        #      Format is x.208 standard YYYYMMDDHHMMSSZ which we will require as GMT time.
-                                                        #                               012345678901234
-
-            birthdate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrBirthday)
-            if birthdate:
-                vcard.addProperty(Property("BDAY", DateTime.parseText(birthdate, fullISO=True)))
-
-            # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
-            #
-            # 3.2.1 ADR Type Definition
-
-            #address
-            # vcard says: Each address attribute can be a string or a list of strings.
-            extended = self.valuesForAttribute(dsattributes.kDSNAttrBuilding, "")
-            street = self.valuesForAttribute(dsattributes.kDSNAttrStreet, "")
-            city = self.valuesForAttribute(dsattributes.kDSNAttrCity, "")
-            region = self.valuesForAttribute(dsattributes.kDSNAttrState, "")
-            code = self.valuesForAttribute(dsattributes.kDSNAttrPostalCode, "")
-            country = self.valuesForAttribute(dsattributes.kDSNAttrCountry, "")
-
-            if len(extended) > 0 or len(street) > 0 or len(city) > 0 or len(region) > 0 or len(code) > 0 or len(country) > 0:
-                vcard.addProperty(Property("ADR",
-                    Adr(
-                        #pobox = box,
-                        extended=extended,
-                        street=street,
-                        locality=city,
-                        region=region,
-                        postalcode=code,
-                        country=country,
-                    ),
-                    params={"TYPE": ["WORK", "PREF", "POSTAL", "PARCEL", ], }
-                ))
-
-            # 3.2.2 LABEL Type Definition
-
-            # dsattributes.kDSNAttrPostalAddress,            # The postal address usually excluding postal code.
-            # dsattributes.kDSNAttrPostalAddressContacts,    # multi-valued attribute that defines a record's alternate postal addresses .
-                                                            #      found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
-            # dsattributes.kDSNAttrAddressLine1,            # Line one of multiple lines of address data for a user.
-            # dsattributes.kDSNAttrAddressLine2,            # Line two of multiple lines of address data for a user.
-            # dsattributes.kDSNAttrAddressLine3,            # Line three of multiple lines of address data for a user.
-
-            for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddress):
-                addUniqueProperty(vcard, Property("LABEL", label, params={"TYPE": ["POSTAL", "PARCEL", ]}), None, dsattributes.kDSNAttrPostalAddress, label)
-
-            for label in self.valuesForAttribute(dsattributes.kDSNAttrPostalAddressContacts):
-                addUniqueProperty(vcard, Property("LABEL", label, params={"TYPE": ["POSTAL", "PARCEL", ]}), None, dsattributes.kDSNAttrPostalAddressContacts, label)
-
-            address = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine1)
-            addressLine2 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine2)
-            if len(addressLine2) > 0:
-                address += "\n" + addressLine2
-            addressLine3 = self.joinedValuesForAttribute(dsattributes.kDSNAttrAddressLine3)
-            if len(addressLine3) > 0:
-                address += "\n" + addressLine3
-
-            if len(address) > 0:
-                vcard.addProperty(Property("LABEL", address, params={"TYPE": ["POSTAL", "PARCEL", ]}))
-
-            # 3.3 TELECOMMUNICATIONS ADDRESSING TYPES http://tools.ietf.org/html/rfc2426#section-3.3
-            # 3.3.1 TEL Type Definition
-            #          TEL;TYPE=work,voice,pref,msg:+1-213-555-1234
-
-            # dsattributes.kDSNAttrPhoneNumber,            # Telephone number of a user.
-            # dsattributes.kDSNAttrMobileNumber,        # Represents the mobile numbers of a user or person.
-                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or
-                                                        #      dsattributes.kDSStdRecordTypePeople).
-            # dsattributes.kDSNAttrFaxNumber,            # Represents the FAX numbers of a user or person.
-                                                        # Usually found in user or people records (kDSStdRecordTypeUsers or
-                                                        # kDSStdRecordTypePeople).
-            # dsattributes.kDSNAttrPagerNumber,            # Represents the pager numbers of a user or person.
-                                                        #      Usually found in user or people records (kDSStdRecordTypeUsers or
-                                                        #      dsattributes.kDSStdRecordTypePeople).
-            # dsattributes.kDSNAttrHomePhoneNumber,        # Home telephone number of a user or person.
-            # dsattributes.kDSNAttrPhoneContacts,        # multi-valued attribute that defines a record's custom phone numbers .
-                                                        #      found in user records (kDSStdRecordTypeUsers).
-                                                        #      Example: home fax:408-555-4444
-
-            params = {"TYPE": ["WORK", "PREF", "VOICE", ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPhoneNumber):
-                addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrPhoneNumber)
-                params = {"TYPE": ["WORK", "VOICE", ], }
-
-            params = {"TYPE": ["WORK", "PREF", "CELL", ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrMobileNumber):
-                addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrMobileNumber)
-                params = {"TYPE": ["WORK", "CELL", ], }
-
-            params = {"TYPE": ["WORK", "PREF", "FAX", ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrFaxNumber):
-                addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrFaxNumber)
-                params = {"TYPE": ["WORK", "FAX", ], }
-
-            params = {"TYPE": ["WORK", "PREF", "PAGER", ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrPagerNumber):
-                addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrPagerNumber)
-                params = {"TYPE": ["WORK", "PAGER", ], }
-
-            params = {"TYPE": ["HOME", "PREF", "VOICE", ], }
-            for phone in self.valuesForAttribute(dsattributes.kDSNAttrHomePhoneNumber):
-                addUniqueProperty(vcard, Property("TEL", phone, params=params), (("TYPE", "PREF"),), phone, dsattributes.kDSNAttrHomePhoneNumber)
-                params = {"TYPE": ["HOME", "VOICE", ], }
-
-            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, "TEL", "work",
-                                                        ["VOICE", "CELL", "FAX", "PAGER", ], {},
-                                                        dsattributes.kDSNAttrPhoneContacts,)
-
-            """
-            # EXTEND:  Use this attribute
-            # dsattributes.kDSNAttrAreaCode,            # Area code of a user's phone number.
-            """
-
-            # 3.3.2 EMAIL Type Definition
-            # dsattributes.kDSNAttrEMailAddress,        # Email address of usually a user record.
-
-            # setup some params
-            preferredWorkParams = {"TYPE": ["WORK", "PREF", "INTERNET", ], }
-            workParams = {"TYPE": ["WORK", "INTERNET", ], }
-            params = preferredWorkParams
-            for emailAddress in self.valuesForAttribute(dsattributes.kDSNAttrEMailAddress):
-                addUniqueProperty(vcard, Property("EMAIL", emailAddress, params=params), (("TYPE", "PREF"),), emailAddress, dsattributes.kDSNAttrEMailAddress)
-                params = workParams
-
-            # dsattributes.kDSNAttrEMailContacts,        # multi-valued attribute that defines a record's custom email addresses .
-                                                        #    found in user records (kDSStdRecordTypeUsers).
-                                                        #      Example: home:johndoe at mymail.com
-
-            # check to see if parameters type are open ended. Could be any string
-            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, "EMAIL", "work",
-                                                        ["WORK", "HOME", ], {},
-                                                        dsattributes.kDSNAttrEMailContacts,)
-
-            """
-            # UNIMPLEMENTED:
-            # 3.3.3 MAILER Type Definition
-            """
-            # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
-            """
-            # UNIMPLEMENTED:
-            # 3.4.1 TZ Type Definition
-            """
-            # 3.4.2 GEO Type Definition
-            #dsattributes.kDSNAttrMapCoordinates,        # attribute that defines coordinates for a user's location .
-                                                        #      Found in user records (kDSStdRecordTypeUsers) and resource records (kDSStdRecordTypeResources).
-                                                        #      Example: 7.7,10.6
-            for coordinate in self.valuesForAttribute(dsattributes.kDSNAttrMapCoordinates):
-                parts = coordinate.split(",")
-                if (len(parts) == 2):
-                    vcard.addProperty(Property("GEO", parts))
-                else:
-                    self.log.info("Ignoring malformed attribute %r with value %r. Well-formed example: 7.7,10.6." % (dsattributes.kDSNAttrMapCoordinates, coordinate))
-            #
-            # 3.5 ORGANIZATIONAL TYPES http://tools.ietf.org/html/rfc2426#section-3.5
-            #
-            # 3.5.1 TITLE Type Definition
-            for jobTitle in self.valuesForAttribute(dsattributes.kDSNAttrJobTitle):
-                addUniqueProperty(vcard, Property("TITLE", jobTitle), None, dsattributes.kDSNAttrJobTitle, jobTitle)
-
-            """
-            # UNIMPLEMENTED:
-            # 3.5.2 ROLE Type Definition
-            # 3.5.3 LOGO Type Definition
-            # 3.5.4 AGENT Type Definition
-            """
-            # 3.5.5 ORG Type Definition
-            company = self.joinedValuesForAttribute(dsattributes.kDSNAttrCompany)
-            if len(company) == 0:
-                company = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationName)
-            department = self.joinedValuesForAttribute(dsattributes.kDSNAttrDepartment)
-            extra = self.joinedValuesForAttribute(dsattributes.kDSNAttrOrganizationInfo)
-            if len(company) > 0 or len(department) > 0:
-                vcard.addProperty(Property("ORG", (company, department, extra,),))
-
-            # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
-            """
-            # UNIMPLEMENTED:
-            # 3.6.1 CATEGORIES Type Definition
-            """
-            # 3.6.2 NOTE Type Definition
-            # dsattributes.kDS1AttrComment,                  # Attribute used for unformatted comment.
-            # dsattributes.kDS1AttrNote,                  # Note attribute. Commonly used in printer records.
-            for comment in self.valuesForAttribute(dsattributes.kDS1AttrComment):
-                addUniqueProperty(vcard, Property("NOTE", comment), None, dsattributes.kDS1AttrComment, comment)
-
-            for note in self.valuesForAttribute(dsattributes.kDS1AttrNote):
-                addUniqueProperty(vcard, Property("NOTE", note), None, dsattributes.kDS1AttrNote, note)
-
-            # 3.6.3 PRODID Type Definition
-            #vcard.addProperty(Property("PRODID", vCardProductID + "//BUILD %s" % twistedcaldav.__version__))
-            #vcard.addProperty(Property("PRODID", vCardProductID))
-            # ADDED WITH CONTSTANT PROPERTIES
-
-            # 3.6.4 REV Type Definition
-            revDate = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrModificationTimestamp)
-            if revDate:
-                vcard.addProperty(Property("REV", DateTime.parseText(revDate, fullISO=True)))
-
-            """
-            # UNIMPLEMENTED:
-            # 3.6.5 SORT-STRING Type Definition
-            # 3.6.6 SOUND Type Definition
-            """
-            # 3.6.7 UID Type Definition
-            # dsattributes.kDS1AttrGeneratedUID,        # Used for 36 character (128 bit) unique ID. Usually found in user,
-                                                        #      group, and computer records. An example value is "A579E95E-CDFE-4EBC-B7E7-F2158562170F".
-                                                        #      The standard format contains 32 hex characters and four hyphen characters.
-            # !! don't use self.guid which is URL encoded
-            vcard.addProperty(Property("UID", self.firstValueForAttribute(dsattributes.kDS1AttrGeneratedUID)))
-
-            # 3.6.8 URL Type Definition
-            # dsattributes.kDSNAttrURL,                    # List of URLs.
-            # dsattributes.kDS1AttrWeblogURI,            # Single-valued attribute that defines the URI of a user's weblog.
-                                                        #     Usually found in user records (kDSStdRecordTypeUsers).
-                                                        #      Example: http://example.com/blog/jsmith
-            for url in self.valuesForAttribute(dsattributes.kDS1AttrWeblogURI):
-                addPropertyAndLabel(groupCount, "weblog", "URL", url, parameters={"TYPE": ["Weblog", ]})
-
-            for url in self.valuesForAttribute(dsattributes.kDSNAttrURL):
-                addPropertyAndLabel(groupCount, "_$!<HomePage>!$_", "URL", url, parameters={"TYPE": ["Homepage", ]})
-
-            # 3.6.9 VERSION Type Definition
-            # ALREADY ADDED
-
-            # 3.7 SECURITY TYPES http://tools.ietf.org/html/rfc2426#section-3.7
-            # 3.7.1 CLASS Type Definition
-            # ALREADY ADDED
-
-            # 3.7.2 KEY Type Definition
-
-            # dsattributes.kDSNAttrPGPPublicKey,        # Pretty Good Privacy public encryption key.
-            # dsattributes.kDS1AttrUserCertificate,        # Attribute containing the binary of the user's certificate.
-                                                        #       Usually found in user records. The certificate is data which identifies a user.
-                                                        #       This data is attested to by a known party, and can be independently verified
-                                                        #       by a third party.
-            # dsattributes.kDS1AttrUserPKCS12Data,        # Attribute containing binary data in PKCS #12 format.
-                                                        #       Usually found in user records. The value can contain keys, certificates,
-                                                        #      and other related information and is encrypted with a passphrase.
-            # dsattributes.kDS1AttrUserSMIMECertificate,# Attribute containing the binary of the user's SMIME certificate.
-                                                        #       Usually found in user records. The certificate is data which identifies a user.
-                                                        #       This data is attested to by a known party, and can be independently verified
-                                                        #       by a third party. SMIME certificates are often used for signed or encrypted
-                                                        #       emails.
-
-            for key in self.valuesForAttribute(dsattributes.kDSNAttrPGPPublicKey):
-                addUniqueProperty(vcard, Property("KEY", key, params={"ENCODING": ["b", ], "TYPE": ["PGPPublicKey", ]}), None, dsattributes.kDSNAttrPGPPublicKey, key)
-
-            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserCertificate):
-                addUniqueProperty(vcard, Property("KEY", key, params={"ENCODING": ["b", ], "TYPE": ["UserCertificate", ]}), None, dsattributes.kDS1AttrUserCertificate, key)
-
-            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserPKCS12Data):
-                addUniqueProperty(vcard, Property("KEY", key, params={"ENCODING": ["b", ], "TYPE": ["UserPKCS12Data", ]}), None, dsattributes.kDS1AttrUserPKCS12Data, key)
-
-            for key in self.valuesForAttribute(dsattributes.kDS1AttrUserSMIMECertificate):
-                addUniqueProperty(vcard, Property("KEY", key, params={"ENCODING": ["b", ], "TYPE": ["UserSMIMECertificate", ]}), None, dsattributes.kDS1AttrUserSMIMECertificate, key)
-
-            """
-            X- attributes, Address Book support
-            """
-            # X-AIM, X-JABBER, X-MSN, X-YAHOO, X-ICQ
-            # instant messaging
-            # dsattributes.kDSNAttrIMHandle,            # Represents the Instant Messaging handles of a user.
-                                                        #      Values should be prefixed with the appropriate IM type
-                                                        #       ie. AIM:, Jabber:, MSN:, Yahoo:, or ICQ:
-                                                        #       Usually found in user records (kDSStdRecordTypeUsers).
-
-            addPropertiesAndLabelsForPrefixedAttribute(groupCount, "X-", None, "aim",
-                                                        ["AIM", "JABBER", "MSN", "YAHOO", "ICQ"],
-                                                        {},
-                                                        dsattributes.kDSNAttrIMHandle,)
-
-            # X-ABRELATEDNAMES
-            # dsattributes.kDSNAttrRelationships,        #      multi-valued attribute that defines the relationship to the record type .
-                                                        #      found in user records (kDSStdRecordTypeUsers).
-                                                        #      Example: brother:John
-            addPropertiesAndLabelsForPrefixedAttribute(groupCount, None, "X-ABRELATEDNAMES", "friend",
-                                                        [],
-                                                        {"FATHER": "_$!<Father>!$_",
-                                                         "MOTHER": "_$!<Mother>!$_",
-                                                         "PARENT": "_$!<Parent>!$_",
-                                                         "BROTHER": "_$!<Brother>!$_",
-                                                         "SISTER": "_$!<Sister>!$_",
-                                                         "CHILD": "_$!<Child>!$_",
-                                                         "FRIEND": "_$!<Friend>!$_",
-                                                         "SPOUSE": "_$!<Spouse>!$_",
-                                                         "PARTNER": "_$!<Partner>!$_",
-                                                         "ASSISTANT": "_$!<Assistant>!$_",
-                                                         "MANAGER": "_$!<Manager>!$_", },
-                                                        dsattributes.kDSNAttrRelationships,)
-
-            # special case for Apple
-            if self.service.appleInternalServer:
-                for manager in self.valuesForAttribute("dsAttrTypeNative:appleManager"):
-                    splitManager = manager.split("|")
-                    if len(splitManager) >= 4:
-                        managerValue = "%s %s, %s" % (splitManager[0], splitManager[1], splitManager[3],)
-                    elif len(splitManager) >= 2:
-                        managerValue = "%s %s" % (splitManager[0], splitManager[1])
-                    else:
-                        managerValue = manager
-                    addPropertyAndLabel(groupCount, "_$!<Manager>!$_", "X-ABRELATEDNAMES", managerValue, parameters={"TYPE": ["Manager", ]})
-
-            """
-            # UNIMPLEMENTED: X- attributes
-
-            X-MAIDENNAME
-            X-PHONETIC-FIRST-NAME
-            X-PHONETIC-MIDDLE-NAME
-            X-PHONETIC-LAST-NAME
-
-            sattributes.kDS1AttrPicture,                # Represents the path of the picture for each user displayed in the login window.
-                                                        #      Found in user records (kDSStdRecordTypeUsers).
-
-            dsattributes.kDS1AttrMapGUID,                # Represents the GUID for a record's map.
-            dsattributes.kDSNAttrMapURI,                # attribute that defines the URI of a user's location.
-
-            dsattributes.kDSNAttrOrganizationInfo,        # Usually the organization info of a user.
-            dsattributes.kDSNAttrAreaCode,                # Area code of a user's phone number.
-
-            dsattributes.kDSNAttrMIME,                    # Data contained in this attribute type is a fully qualified MIME Type.
-
-            """
-
-            # debug, create x attributes for all ds attributes
-            if self.service.addDSAttrXProperties:
-                for attribute in self.originalAttributes:
-                    for value in self.valuesForAttribute(attribute):
-                        vcard.addProperty(Property("X-" + "-".join(attribute.split(":")), removeControlChars(value)))
-
-            return vcard
-
-        if not self._vCard:
-            self._vCard = generateVCard()
-
-        return self._vCard
-
-
-    def vCardText(self):
-        if not self._vCardText:
-            self._vCardText = str(self.vCard())
-
-        return self._vCardText
-
-
-    def uriName(self):
-        if not self._uriName:
-            self._uriName = self.vCard().getProperty("UID").value() + ".vcf"
-        #print("uriName():self._uriName=%s" % self._uriName)
-        return self._uriName
-
-
-    def hRef(self, parentURI="/directory/"):
-        if not self._hRef:
-            self._hRef = davxml.HRef.fromString(joinURL(parentURI, self.uriName()))
-
-        return self._hRef
-
-
-    def readProperty(self, property, request):
-
-        if type(property) is tuple:
-            qname = property
-        else:
-            qname = property.qname()
-
-        namespace, name = qname
-
-        #print("VCardResource.readProperty: qname = %s" % (qname, ))
-
-        if namespace == dav_namespace:
-            if name == "resourcetype":
-                result = davxml.ResourceType.empty #@UndefinedVariable
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-            elif name == "getetag":
-                result = davxml.GETETag(ETag(hashlib.md5(self.vCardText()).hexdigest()).generate())
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-            elif name == "getcontenttype":
-                mimeType = MimeType('text', 'vcard', {})
-                result = davxml.GETContentType(generateContentType(mimeType))
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-            elif name == "getcontentlength":
-                result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-            elif name == "getlastmodified":
-                if self.vCard().hasProperty("REV"):
-                    modDatetime = parse_date(self.vCard().propertyValue("REV"))
-                else:
-                    # use creation date attribute if it exists
-                    creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
-                    if creationDateString:
-                        modDatetime = parse_date(creationDateString)
-                    else:
-                        modDatetime = datetime.datetime.utcnow()
-
-                #strip time zone because time zones are unimplemented in davxml.GETLastModified.fromDate
-                d = modDatetime.date()
-                t = modDatetime.time()
-                modDatetimeNoTZ = datetime.datetime(d.year, d.month, d.day, t.hour, t.minute, t.second, t.microsecond, None)
-                result = davxml.GETLastModified.fromDate(modDatetimeNoTZ)
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-            elif name == "creationdate":
-                creationDateString = self.isoDateStringForDateAttribute(dsattributes.kDS1AttrCreationTimestamp)
-                if creationDateString:
-                    creationDatetime = parse_date(creationDateString)
-                elif self.vCard().hasProperty("REV"):    # use modification date property if it exists
-                    creationDatetime = parse_date(self.vCard().propertyValue("REV"))
-                else:
-                    creationDatetime = datetime.datetime.utcnow()
-                result = davxml.CreationDate.fromDate(creationDatetime)
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-            elif name == "displayname":
-                # AddressBook.app uses N. Use FN or UID instead?
-                result = davxml.DisplayName.fromString(self.vCard().propertyValue("N"))
-                #print("VCardResource.readProperty: qname = %s, result = %s" % (qname, result))
-                return result
-
-        elif namespace == twisted_dav_namespace:
-            return super(VCardRecord, self).readProperty(property, request)
-            #return DAVPropertyMixIn.readProperty(self, property, request)
-
-        return self.directoryBackedAddressBook.readProperty(property, request)
-
-
-    def listProperties(self, request):
-        #print("VCardResource.listProperties()")
-        qnames = set(self.liveProperties())
-
-        # Add dynamic live properties that exist
-        dynamicLiveProperties = (
-            (dav_namespace, "quota-available-bytes"),
-            (dav_namespace, "quota-used-bytes"),
-        )
-        for dqname in dynamicLiveProperties:
-            #print("VCardResource.listProperties: removing dqname=%s" % (dqname,))
-            qnames.remove(dqname)
-
-        for qname in self.deadProperties().list():
-            if (qname not in qnames) and (qname[0] != twisted_private_namespace):
-                #print("listProperties: adding qname=%s" % (qname,))
-                qnames.add(qname)
-
-        #for qn in qnames: print("VCardResource.listProperties: qn=%s" % (qn,))
-
-        yield qnames
-
-    listProperties = deferredGenerator(listProperties)
-
-
-
-# utility
-#remove control characters because vCard does not support them
-def removeControlChars(utf8String):
-    result = utf8String
-    for a in utf8String:
-        if '\x00' <= a <= '\x1F':
-            result = ""
-            for c in utf8String:
-                if '\x00' <= c <= '\x1F':
-                    pass
-                else:
-                    result += c
-    #if utf8String != result: print ("changed %r to %r" % (utf8String, result))
-    return result

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/principal.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/principal.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -28,55 +28,55 @@
     "DirectoryCalendarPrincipalResource",
 ]
 
-from urllib import unquote
+from urllib import quote, unquote
 from urlparse import urlparse
+import uuid
 
+from twext.python.log import Logger
 from twisted.cred.credentials import UsernamePassword
-from twisted.python.failure import Failure
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.defer import succeed
-from twisted.web.template import XMLFile, Element, renderer, tags
-from twistedcaldav.directory.util import NotFoundResource
-
-from txweb2.auth.digest import DigestedCredentials
-from txweb2 import responsecode
-from txweb2.http import HTTPError
-from txdav.xml import element as davxml
-from txweb2.dav.util import joinURL
-from txweb2.dav.noneprops import NonePropertyStore
-
-from twext.python.log import Logger
-
-
-try:
-    from twistedcaldav.authkerb import NegotiateCredentials
-    NegotiateCredentials # sigh, pyflakes
-except ImportError:
-    NegotiateCredentials = None
 from twisted.python.modules import getModule
-
+from twisted.web.template import XMLFile, Element, renderer
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav.cache import DisabledCacheNotifier, PropfindCacheMixin
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.directory.augment import allowedAutoScheduleModes
 from twistedcaldav.directory.common import uidsResourceName
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
-from twistedcaldav.directory.idirectory import IDirectoryService
-from twistedcaldav.directory.wiki import getWikiACL
+from twistedcaldav.directory.util import NotFoundResource
+from twistedcaldav.directory.util import (
+    formatLink, formatLinks, formatPrincipals, formatList
+)
+from txdav.who.wiki import getWikiACL
+from twistedcaldav.extensions import (
+    ReadOnlyResourceMixIn, DAVPrincipalResource, DAVResourceWithChildrenMixin
+)
 from twistedcaldav.extensions import DirectoryElement
-from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVPrincipalResource, \
-    DAVResourceWithChildrenMixin
 from twistedcaldav.resource import CalendarPrincipalCollectionResource, CalendarPrincipalResource
 from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
+from txdav.who.delegates import RecordType as DelegateRecordType
+from txdav.who.directory import CalendarDirectoryRecordMixin
+from txdav.xml import element as davxml
+from txweb2 import responsecode
+from txweb2.auth.digest import DigestedCredentials
+from txweb2.dav.noneprops import NonePropertyStore
+from txweb2.dav.util import joinURL
+from txweb2.http import HTTPError
 
+try:
+    from twistedcaldav.authkerb import NegotiateCredentials
+    NegotiateCredentials  # sigh, pyflakes
+except ImportError:
+    NegotiateCredentials = None
+
 thisModule = getModule(__name__)
 log = Logger()
 
 
 class PermissionsMixIn (ReadOnlyResourceMixIn):
     def defaultAccessControlList(self):
-        return authReadACL
+        return succeed(authReadACL)
 
 
     @inlineCallbacks
@@ -94,7 +94,7 @@
         else:
             # ...otherwise permissions are fixed, and are not subject to
             # inheritance rules, etc.
-            returnValue(self.defaultAccessControlList())
+            returnValue((yield self.defaultAccessControlList()))
 
 
 
@@ -108,7 +108,7 @@
 def cuTypeConverter(cuType):
     """ Converts calendar user types to OD type names """
 
-    return "recordType", DirectoryRecord.fromCUType(cuType)
+    return "recordType", CalendarDirectoryRecordMixin.fromCUType(cuType)
 
 
 
@@ -118,7 +118,7 @@
     cua = normalizeCUAddr(origCUAddr)
 
     if cua.startswith("urn:uuid:"):
-        return "guid", cua[9:]
+        return "guid", uuid.UUID(cua[9:])
 
     elif cua.startswith("mailto:"):
         return "emailAddresses", cua[7:]
@@ -126,7 +126,7 @@
     elif cua.startswith("/") or cua.startswith("http"):
         ignored, collection, id = cua.rsplit("/", 2)
         if collection == "__uids__":
-            return "guid", id
+            return "uid", id
         else:
             return "recordName", id
 
@@ -150,18 +150,21 @@
         CalendarPrincipalCollectionResource.__init__(self, url)
         DAVResourceWithChildrenMixin.__init__(self)
 
-        self.directory = IDirectoryService(directory)
+        # MOVE2WHO
+        # self.directory = IDirectoryService(directory)
+        self.directory = directory
 
 
     def __repr__(self):
         return "<%s: %s %s>" % (self.__class__.__name__, self.directory, self._url)
 
 
+    @inlineCallbacks
     def locateChild(self, req, segments):
-        child = self.getChild(segments[0])
+        child = (yield self.getChild(segments[0]))
         if child is not None:
-            return (child, segments[1:])
-        return (NotFoundResource(principalCollections=self.principalCollections()), ())
+            returnValue((child, segments[1:]))
+        returnValue((NotFoundResource(principalCollections=self.principalCollections()), ()))
 
 
     def deadProperties(self):
@@ -174,27 +177,30 @@
         return succeed(None)
 
 
+    @inlineCallbacks
     def principalForShortName(self, recordType, name):
-        return self.principalForRecord(self.directory.recordWithShortName(recordType, name))
+        record = (yield self.directory.recordWithShortName(recordType, name))
+        returnValue((yield self.principalForRecord(record)))
 
 
     def principalForUser(self, user):
-        return self.principalForShortName(DirectoryService.recordType_users, user)
+        return self.principalForShortName(self.directory.recordType.lookupByName("user"), user)
 
 
+    @inlineCallbacks
     def principalForAuthID(self, user):
         # Basic/Digest creds -> just lookup user name
         if isinstance(user, UsernamePassword) or isinstance(user, DigestedCredentials):
-            return self.principalForUser(user.username)
+            returnValue((yield self.principalForUser(user.username)))
         elif NegotiateCredentials is not None and isinstance(user, NegotiateCredentials):
             authID = "Kerberos:%s" % (user.principal,)
-            principal = self.principalForRecord(self.directory.recordWithAuthID(authID))
+            principal = yield self.principalForRecord((yield self.directory.recordWithAuthID(authID)))
             if principal:
-                return principal
+                returnValue(principal)
             elif user.username:
-                return self.principalForUser(user.username)
+                returnValue((yield self.principalForUser(user.username)))
 
-        return None
+        returnValue(None)
 
 
     def principalForUID(self, uid):
@@ -207,7 +213,7 @@
 
     def principalForRecord(self, record):
         if record is None or not record.enabled:
-            return None
+            return succeed(None)
         return self.principalForUID(record.uid)
 
     ##
@@ -217,7 +223,7 @@
     _cs_ns = "http://calendarserver.org/ns/"
     _fieldMap = {
         ("DAV:" , "displayname") :
-            ("fullName", None, "Display Name", davxml.DisplayName),
+            ("fullNames", None, "Display Name", davxml.DisplayName),
         ("urn:ietf:params:xml:ns:caldav" , "calendar-user-type") :
             ("", cuTypeConverter, "Calendar User Type",
             caldavxml.CalendarUserType),
@@ -281,16 +287,36 @@
         #
         # Create children
         #
-        for recordType in self.directory.recordTypes():
-            self.putChild(recordType, DirectoryPrincipalTypeProvisioningResource(self, recordType))
 
+        self.supportedChildTypes = (
+            self.directory.recordType.user,
+            self.directory.recordType.group,
+            self.directory.recordType.location,
+            self.directory.recordType.resource,
+            self.directory.recordType.address,
+        )
+
+        for name, recordType in [
+            (self.directory.recordTypeToOldName(r), r)
+            for r in self.supportedChildTypes
+        ]:
+            self.putChild(
+                name,
+                DirectoryPrincipalTypeProvisioningResource(
+                    self, name, recordType
+                )
+            )
+
         self.putChild(uidsResourceName, DirectoryPrincipalUIDProvisioningResource(self))
 
 
+    @inlineCallbacks
     def principalForUID(self, uid):
-        return self.getChild(uidsResourceName).getChild(uid)
+        child = (yield self.getChild(uidsResourceName))
+        returnValue((yield child.getChild(uid)))
 
 
+    @inlineCallbacks
     def _principalForURI(self, uri):
         scheme, netloc, path, _ignore_params, _ignore_query, _ignore_fragment = urlparse(uri)
 
@@ -312,56 +338,62 @@
 
             if (host != config.ServerHostName and
                 host not in config.Scheduling.Options.PrincipalHostAliases):
-                return None
+                returnValue(None)
 
             if port != {
                 "http" : config.HTTPPort,
                 "https": config.SSLPort,
             }[scheme]:
-                return None
+                returnValue(None)
 
         elif scheme == "urn":
             if path.startswith("uuid:"):
-                return self.principalForUID(path[5:])
+                returnValue((yield self.principalForUID(path[5:])))
             else:
-                return None
+                returnValue(None)
         else:
-            return None
+            returnValue(None)
 
         if not path.startswith(self._url):
-            return None
+            returnValue(None)
 
         path = path[len(self._url) - 1:]
 
         segments = [unquote(s) for s in path.rstrip("/").split("/")]
         if segments[0] == "" and len(segments) == 3:
-            typeResource = self.getChild(segments[1])
+            typeResource = yield self.getChild(segments[1])
             if typeResource is not None:
-                principalResource = typeResource.getChild(segments[2])
+                principalResource = yield typeResource.getChild(segments[2])
                 if principalResource:
-                    return principalResource
+                    returnValue(principalResource)
 
-        return None
+        returnValue(None)
 
 
+    @inlineCallbacks
     def principalForCalendarUserAddress(self, address):
         # First see if the address is a principal URI
-        principal = self._principalForURI(address)
+        principal = yield self._principalForURI(address)
         if principal:
-            if isinstance(principal, DirectoryCalendarPrincipalResource) and principal.record.enabledForCalendaring:
-                return principal
+            if (
+                isinstance(principal, DirectoryCalendarPrincipalResource) and
+                principal.record.hasCalendars
+            ):
+                returnValue(principal)
         else:
             # Next try looking it up in the directory
-            record = self.directory.recordWithCalendarUserAddress(address)
-            if record is not None and record.enabled and record.enabledForCalendaring:
-                return self.principalForRecord(record)
+            record = yield self.directory.recordWithCalendarUserAddress(address)
+            if record is not None and record.hasCalendars:
+                returnValue((yield self.principalForRecord(record)))
 
         log.debug("No principal for calendar user address: %r" % (address,))
-        return None
+        returnValue(None)
 
 
+    @inlineCallbacks
     def principalForRecord(self, record):
-        return self.getChild(uidsResourceName).principalForRecord(record)
+        child = (yield self.getChild(uidsResourceName))
+        returnValue((yield child.principalForRecord(record)))
 
 
     ##
@@ -375,13 +407,16 @@
 
     def getChild(self, name):
         if name == "":
-            return self
+            return succeed(self)
         else:
-            return self.putChildren.get(name, None)
+            return succeed(self.putChildren.get(name, None))
 
 
     def listChildren(self):
-        return self.directory.recordTypes()
+        return [
+            self.directory.recordTypeToOldName(r) for r in
+            self.supportedChildTypes
+        ]
 
 
     ##
@@ -392,43 +427,20 @@
         return (self,)
 
 
-    ##
-    # Proxy callback from directory service
-    ##
 
-    def isProxyFor(self, record1, record2):
-        """
-        Test whether the principal identified by directory record1 is a proxy for the principal identified by
-        record2.
-
-        @param record1: directory record for a user
-        @type record1: L{DirectoryRecord}
-        @param record2: directory record to test with
-        @type record2: L{DirectoryRercord}
-
-        @return: C{True} if record1 is a proxy for record2, otherwise C{False}
-        @rtype: C{bool}
-        """
-
-        principal1 = self.principalForUID(record1.uid)
-        principal2 = self.principalForUID(record2.uid)
-        return principal1.isProxyFor(principal2)
-
-
-
 class DirectoryPrincipalTypeProvisioningResource (DirectoryProvisioningResource):
     """
     Collection resource which provisions directory principals of a
     specific type as its children, indexed by short name.
     """
-    def __init__(self, parent, recordType):
+    def __init__(self, parent, name, recordType):
         """
         @param parent: the parent L{DirectoryPrincipalProvisioningResource}.
         @param recordType: the directory record type to provision.
         """
         DirectoryProvisioningResource.__init__(
             self,
-            joinURL(parent.principalCollectionURL(), recordType) + "/",
+            joinURL(parent.principalCollectionURL(), name) + "/",
             parent.directory
         )
 
@@ -459,22 +471,26 @@
 
     def getChild(self, name):
         if name == "":
-            return self
+            return succeed(self)
         else:
             return self.principalForShortName(self.recordType, name)
 
 
+    @inlineCallbacks
     def listChildren(self):
+        children = []
         if config.EnablePrincipalListings:
+            try:
+                for record in (
+                    yield self.directory.recordsWithRecordType(self.recordType)
+                ):
+                    for shortName in record.shortNames:
+                        children.append(shortName)
+            except AttributeError:
+                log.warn("Cannot list children of record type {rt}",
+                         rt=self.recordType.name)
+            returnValue(children)
 
-
-            def _recordShortnameExpand():
-                for record in self.directory.listRecords(self.recordType):
-                    if record.enabled:
-                        for shortName in record.shortNames:
-                            yield shortName
-
-            return _recordShortnameExpand()
         else:
             # Not a listable collection
             raise HTTPError(responsecode.FORBIDDEN)
@@ -517,16 +533,16 @@
 
 
     def principalForRecord(self, record):
-        if record is None or not record.enabled:
-            return None
+        if record is None:
+            return succeed(None)
 
-        if record.enabledForCalendaring or record.enabledForAddressBooks:
+        if record.hasCalendars or record.hasContacts:
             # XXX these are different features and one should not automatically
             # imply the other...
             principal = DirectoryCalendarPrincipalResource(self, record)
         else:
             principal = DirectoryPrincipalResource(self, record)
-        return principal
+        return succeed(principal)
 
     ##
     # Static
@@ -538,9 +554,10 @@
         raise HTTPError(responsecode.NOT_FOUND)
 
 
+    @inlineCallbacks
     def getChild(self, name):
         if name == "":
-            return self
+            returnValue(self)
 
         if "#" in name:
             # This UID belongs to a sub-principal
@@ -549,16 +566,16 @@
             primaryUID = name
             subType = None
 
-        record = self.directory.recordWithUID(primaryUID)
-        primaryPrincipal = self.principalForRecord(record)
+        record = (yield self.directory.recordWithUID(primaryUID))
+        primaryPrincipal = (yield self.principalForRecord(record))
         if primaryPrincipal is None:
             log.info("No principal found for UID: %s" % (name,))
-            return None
+            returnValue(None)
 
         if subType is None:
-            return primaryPrincipal
+            returnValue(primaryPrincipal)
         else:
-            return primaryPrincipal.getChild(subType)
+            returnValue((yield primaryPrincipal.getChild(subType)))
 
 
     def listChildren(self):
@@ -610,17 +627,31 @@
         Top-level renderer in the template.
         """
         record = self.resource.record
+        try:
+            if isinstance(record.guid, uuid.UUID):
+                guid = str(record.guid).upper()
+            else:
+                guid = record.guid
+        except AttributeError:
+            guid = ""
+        try:
+            emailAddresses = record.emailAddresses
+        except AttributeError:
+            emailAddresses = []
         return tag.fillSlots(
             directoryGUID=str(record.service.guid),
-            realm=str(record.service.realmName),
-            principalGUID=str(record.guid),
-            recordType=str(record.recordType),
-            shortNames=",".join(record.shortNames),
-            securityIDs=",".join(record.authIDs),
-            fullName=str(record.fullName),
-            firstName=str(record.firstName),
-            lastName=str(record.lastName),
-            emailAddresses=formatList(record.emailAddresses),
+            realm=record.service.realmName.encode("utf-8"),
+            principalGUID=guid,
+            recordType=record.service.recordTypeToOldName(record.recordType),
+            shortNames=",".join([n.encode("utf-8") for n in record.shortNames]),
+            # MOVE2WHO: need this?
+            # securityIDs=",".join(record.authIDs),
+            fullName=record.displayName.encode("utf-8"),
+            # MOVE2WHO: need this?
+            # firstName=str(record.firstName),
+            # MOVE2WHO: need this?
+            # lastName=str(record.lastName),
+            emailAddresses=formatList(emailAddresses),
             principalUID=str(self.resource.principalUID()),
             principalURL=formatLink(self.resource.principalURL()),
             alternateURIs=formatLinks(self.resource.alternateURIs()),
@@ -697,7 +728,7 @@
         """
         resource = self.resource
         record = resource.record
-        if record.enabledForCalendaring:
+        if record.hasCalendars:
             return tag.fillSlots(
                 calendarUserAddresses=formatLinks(
                     sorted(resource.calendarUserAddresses())
@@ -715,7 +746,7 @@
         """
         resource = self.resource
         record = resource.record
-        if record.enabledForAddressBooks:
+        if record.hasContacts:
             return tag.fillSlots(
                 addressBookHomes=formatLinks(resource.addressBookHomeURLs())
             )
@@ -779,7 +810,12 @@
         self._url = url
 
         self._alternate_urls = tuple([
-            joinURL(parent.parent.principalCollectionURL(), record.recordType, shortName) + slash for shortName in record.shortNames
+            joinURL(
+                parent.parent.principalCollectionURL(),
+                record.service.recordTypeToOldName(record.recordType),
+                quote(shortName.encode("utf-8"))
+            ) + slash
+            for shortName in record.shortNames
         ])
 
 
@@ -791,7 +827,10 @@
         """
         Principals are the same if their principalURLs are the same.
         """
-        return (self.principalURL() == other.principalURL()) if isinstance(other, DirectoryPrincipalResource) else False
+        if isinstance(other, DirectoryPrincipalResource):
+            return (self.principalURL() == other.principalURL())
+        else:
+            return False
 
 
     def __ne__(self, other):
@@ -812,25 +851,34 @@
         namespace, name = qname
 
         if qname == davxml.ResourceID.qname():
-            returnValue(davxml.ResourceID(davxml.HRef.fromString("urn:uuid:%s" % (self.record.guid,))))
+            # FIXME: should this return a different CUA flavor if guid is not set on this record?
+            if hasattr(self.record, "guid"):
+                returnValue(davxml.ResourceID(davxml.HRef.fromString("urn:uuid:%s" % (self.record.guid,))))
+
         elif namespace == calendarserver_namespace:
-            if name == "first-name":
-                firstName = self.record.firstName
-                if firstName is not None:
-                    returnValue(customxml.FirstNameProperty(firstName))
-                else:
-                    returnValue(None)
 
-            elif name == "last-name":
-                lastName = self.record.lastName
-                if lastName is not None:
-                    returnValue(customxml.LastNameProperty(lastName))
-                else:
-                    returnValue(None)
+            # MOVE2WHO
+            # if name == "first-name":
+            #     firstName = self.record.firstName
+            #     if firstName is not None:
+            #         returnValue(customxml.FirstNameProperty(firstName))
+            #     else:
+            #         returnValue(None)
 
-            elif name == "email-address-set":
+            # elif name == "last-name":
+            #     lastName = self.record.lastName
+            #     if lastName is not None:
+            #         returnValue(customxml.LastNameProperty(lastName))
+            #     else:
+            #         returnValue(None)
+
+            if name == "email-address-set":
+                try:
+                    emails = self.record.emailAddresses
+                except AttributeError:
+                    emails = []
                 returnValue(customxml.EmailAddressSet(
-                    *[customxml.EmailAddressProperty(addr) for addr in sorted(self.record.emailAddresses)]
+                    *[customxml.EmailAddressProperty(addr) for addr in sorted(emails)]
                 ))
 
         result = (yield super(DirectoryPrincipalResource, self).readProperty(property, request))
@@ -867,7 +915,7 @@
 
 
     def displayName(self):
-        return self.record.displayName()
+        return self.record.displayName
 
     ##
     # ACL
@@ -939,51 +987,48 @@
 
 
     @inlineCallbacks
-    def proxyFor(self, read_write, resolve_memberships=True):
+    def proxyFor(self, readWrite):
+        """
+        Returns the set of principals currently delegating to this principal
+        with the access indicated by the readWrite argument.  If readWrite is
+        True, then write-access delegators are returned, otherwise the read-
+        only-access delegators are returned.
 
+        @param readWrite: Whether to look up read-write delegators, or
+            read-only delegators
+        @type readWrite: C{bool}
+
+        @return: A Deferred firing with a set of principals
+        """
         proxyFors = set()
 
-        if resolve_memberships:
-            cache = getattr(self.record.service, "groupMembershipCache", None)
-            if cache:
-                log.debug("proxyFor is using groupMembershipCache")
-                guids = (yield self.record.cachedGroups())
-                memberships = set()
-                for guid in guids:
-                    principal = self.parent.principalForUID(guid)
-                    if principal:
-                        memberships.add(principal)
-            else:
-                memberships = self._getRelatives("groups", infinity=True)
-
-            for membership in memberships:
-                results = (yield membership.proxyFor(read_write, False))
-                proxyFors.update(results)
-
         if config.EnableProxyPrincipals:
-            # Get proxy group UIDs and map to principal resources
-            proxies = []
-            memberships = (yield self._calendar_user_proxy_index().getMemberships(self.principalUID()))
-            for uid in memberships:
-                subprincipal = self.parent.principalForUID(uid)
-                if subprincipal:
-                    if subprincipal.isProxyType(read_write):
-                        proxies.append(subprincipal.parent)
-                else:
-                    yield self._calendar_user_proxy_index().removeGroup(uid)
+            proxyRecordType = (
+                DelegateRecordType.writeDelegatorGroup if readWrite else
+                DelegateRecordType.readDelegatorGroup
+            )
+            proxyGroupRecord = yield self.record.service.recordWithShortName(
+                proxyRecordType, self.record.uid
+            )
+            if proxyGroupRecord is not None:
+                proxyForRecords = yield proxyGroupRecord.members()
 
-            proxyFors.update(proxies)
+                uids = set()
+                for record in tuple(proxyForRecords):
+                    if record.uid in uids:
+                        proxyForRecords.remove(record)
+                    else:
+                        uids.add(record.uid)
 
-        uids = set()
-        for principal in tuple(proxyFors):
-            if principal.principalUID() in uids:
-                proxyFors.remove(principal)
-            else:
-                uids.add(principal.principalUID())
+                for record in proxyForRecords:
+                    principal = yield self.parent.principalForRecord(record)
+                    if principal is not None:
+                        proxyFors.add(principal)
 
         returnValue(proxyFors)
 
 
+    @inlineCallbacks
     def _getRelatives(self, method, record=None, relatives=None, records=None, proxy=None, infinity=False):
         if record is None:
             record = self.record
@@ -994,62 +1039,51 @@
 
         if record not in records:
             records.add(record)
-            for relative in getattr(record, method)():
+            for relative in (yield getattr(record, method)()):
                 if relative not in records:
-                    found = self.parent.principalForRecord(relative)
+                    found = (yield self.parent.principalForRecord(relative))
                     if found is None:
                         log.error("No principal found for directory record: %r" % (relative,))
                     else:
                         if proxy:
                             if proxy == "read-write":
-                                found = found.getChild("calendar-proxy-write")
+                                found = (yield found.getChild("calendar-proxy-write"))
                             else:
-                                found = found.getChild("calendar-proxy-read")
+                                found = (yield found.getChild("calendar-proxy-read"))
                         relatives.add(found)
 
                     if infinity:
-                        self._getRelatives(method, relative, relatives, records,
+                        yield self._getRelatives(method, relative, relatives, records,
                             infinity=infinity)
 
-        return relatives
+        returnValue(relatives)
 
 
     def groupMembers(self):
-        return succeed(self._getRelatives("members"))
+        return self._getRelatives("members")
 
 
     def expandedGroupMembers(self):
-        return succeed(self._getRelatives("members", infinity=True))
+        return self._getRelatives("members", infinity=True)
 
 
     @inlineCallbacks
     def groupMemberships(self, infinity=False):
 
-        cache = getattr(self.record.service, "groupMembershipCache", None)
-        if cache:
-            log.debug("groupMemberships is using groupMembershipCache")
-            guids = (yield self.record.cachedGroups())
-            groups = set()
-            for guid in guids:
-                principal = self.parent.principalForUID(guid)
-                if principal:
-                    groups.add(principal)
-        else:
-            groups = self._getRelatives("groups", infinity=infinity)
+        groups = yield self._getRelatives("groups", infinity=infinity)
 
         if config.EnableProxyPrincipals:
-            # Get proxy group UIDs and map to principal resources
-            proxies = []
-            memberships = (yield self._calendar_user_proxy_index().getMemberships(self.principalUID()))
-            for uid in memberships:
-                subprincipal = self.parent.principalForUID(uid)
-                if subprincipal:
-                    proxies.append(subprincipal)
-                else:
-                    yield self._calendar_user_proxy_index().removeGroup(uid)
+            for readWrite, proxyType in (
+                (True, "calendar-proxy-write"),
+                (False, "calendar-proxy-read")
+            ):
+                proxyFors = yield self.proxyFor(readWrite)
+                for proxyFor in proxyFors:
+                    subPrincipal = yield self.parent.principalForUID(
+                        "{}#{}".format(proxyFor.record.uid, proxyType)
+                    )
+                    groups.add(subPrincipal)
 
-            groups.update(proxies)
-
         returnValue(groups)
 
 
@@ -1086,28 +1120,13 @@
         return self.record.thisServer()
 
 
-    ##
-    # Extra resource info
-    ##
-
-    @inlineCallbacks
-    def setAutoSchedule(self, autoSchedule):
-        self.record.autoSchedule = autoSchedule
-        augmentRecord = (yield self.record.service.augmentService.getAugmentRecord(self.record.guid, self.record.recordType))
-        augmentRecord.autoSchedule = autoSchedule
-        (yield self.record.service.augmentService.addAugmentRecords([augmentRecord]))
-
-
-    def getAutoSchedule(self):
-        return self.record.autoSchedule
-
-
     def canAutoSchedule(self, organizer=None):
         """
         Determine the auto-schedule state based on record state, type and config settings.
 
         @param organizer: the CUA of the organizer trying to schedule this principal
         @type organizer: C{str}
+        @return: C{Deferred} firing a C{bool}
         """
         return self.record.canAutoSchedule(organizer)
 
@@ -1131,9 +1150,8 @@
 
         @param organizer: the CUA of the organizer scheduling this principal
         @type organizer: C{str}
-        @return: auto schedule mode; one of: none, accept-always, decline-always,
-            accept-if-free, decline-if-busy, automatic (see stdconfig.py)
-        @rtype: C{str}
+        @return: auto schedule mode
+        @rtype: C{Deferred} firing L{AutoScheduleMode}
         """
         return self.record.getAutoScheduleMode(organizer)
 
@@ -1169,7 +1187,7 @@
         @type organizer: C{str}
         @return: True if the autoAcceptGroup is assigned, and the organizer is a member
             of that group.  False otherwise.
-        @rtype: C{bool}
+        @rtype: C{Deferred} firing C{bool}
         """
         return self.record.autoAcceptFromOrganizer()
 
@@ -1187,18 +1205,19 @@
         raise HTTPError(responsecode.NOT_FOUND)
 
 
+    @inlineCallbacks
     def locateChild(self, req, segments):
-        child = self.getChild(segments[0])
+        child = (yield self.getChild(segments[0]))
         if child is not None:
-            return (child, segments[1:])
-        return (None, ())
+            returnValue((child, segments[1:]))
+        returnValue((None, ()))
 
 
     def getChild(self, name):
         if name == "":
-            return self
+            return succeed(self)
 
-        return None
+        return succeed(None)
 
 
     def listChildren(self):
@@ -1221,7 +1240,7 @@
 
 
     def addressBooksEnabled(self):
-        return config.EnableCardDAV and self.record.enabledForAddressBooks
+        return config.EnableCardDAV and self.record.hasContacts
 
 
     @inlineCallbacks
@@ -1254,6 +1273,7 @@
         Return a CUA for this principal, preferring in this order:
             urn:uuid: form
             mailto: form
+            /principal/__uids__/ form
             first in calendarUserAddresses( ) list
         """
         return self.record.canonicalCalendarUserAddress()
@@ -1288,7 +1308,7 @@
 
 
     def calendarHomeURLs(self):
-        if self.record.enabledForCalendaring:
+        if self.record.hasCalendars:
             homeURL = self._homeChildURL(None)
         else:
             homeURL = ""
@@ -1318,7 +1338,7 @@
 
 
     def addressBookHomeURLs(self):
-        if self.record.enabledForAddressBooks:
+        if self.record.hasContacts:
             homeURL = self._addressBookHomeChildURL(None)
         else:
             homeURL = ""
@@ -1391,22 +1411,26 @@
 
     def getChild(self, name):
         if name == "":
-            return self
+            return succeed(self)
 
-        if config.EnableProxyPrincipals and name in ("calendar-proxy-read",
-                                                     "calendar-proxy-write"):
+        if config.EnableProxyPrincipals and name in (
+            "calendar-proxy-read", "calendar-proxy-write",
+            "calendar-proxy-read-for", "calendar-proxy-write-for",
+            ):
             # name is required to be str
             from twistedcaldav.directory.calendaruserproxy import (
                 CalendarUserProxyPrincipalResource
             )
-            return CalendarUserProxyPrincipalResource(self, str(name))
+            return succeed(CalendarUserProxyPrincipalResource(self, str(name)))
         else:
-            return None
+            return succeed(None)
 
 
     def listChildren(self):
         if config.EnableProxyPrincipals:
-            return ("calendar-proxy-read", "calendar-proxy-write")
+            return (
+                "calendar-proxy-read", "calendar-proxy-write",
+            )
         else:
             return ()
 
@@ -1425,71 +1449,3 @@
 
 
 
-def formatPrincipals(principals):
-    """
-    Format a list of principals into some twisted.web.template DOM objects.
-    """
-    def recordKey(principal):
-        try:
-            record = principal.record
-        except AttributeError:
-            try:
-                record = principal.parent.record
-            except:
-                return None
-        return (record.recordType, record.shortNames[0])
-
-
-    def describe(principal):
-        if hasattr(principal, "record"):
-            return " - %s" % (principal.record.fullName,)
-        else:
-            return ""
-
-    return formatList(
-        tags.a(href=principal.principalURL())(
-            str(principal), describe(principal)
-        )
-        for principal in sorted(principals, key=recordKey)
-    )
-
-
-
-def formatList(iterable):
-    """
-    Format a list of stuff as an interable.
-    """
-    thereAreAny = False
-    try:
-        item = None
-        for item in iterable:
-            thereAreAny = True
-            yield " -> "
-            if item is None:
-                yield "None"
-            else:
-                yield item
-            yield "\n"
-    except Exception, e:
-        log.error("Exception while rendering: %s" % (e,))
-        Failure().printTraceback()
-        yield "  ** %s **: %s\n" % (e.__class__.__name__, e)
-    if not thereAreAny:
-        yield " '()\n"
-
-
-
-def formatLink(url):
-    """
-    Convert a URL string into some twisted.web.template DOM objects for
-    rendering as a link to itself.
-    """
-    return tags.a(href=url)(url)
-
-
-
-def formatLinks(urls):
-    """
-    Format a list of URL strings as a list of twisted.web.template DOM links.
-    """
-    return formatList(formatLink(link) for link in urls)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/accounts.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/accounts.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/accounts.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,279 +18,382 @@
 
 <!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
 
-<accounts realm="Test">
-  <user>
-    <uid>admin</uid>
-    <guid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</guid>
+<directory realm="Test">
+  <record type="user">
+    <short-name>admin</short-name>
+    <uid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</uid>
     <password>nimda</password>
-    <name>Administrators</name>
-  </user>
-  <user>
-    <uid>wsanchez</uid>
+    <full-name>Administrators</full-name>
+  </record>
+  <record type="user">
+    <short-name>wsanchez</short-name>
+    <uid>6423F94A-6B76-4A3A-815B-D52CFD77935D</uid>
     <guid>6423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
     <password>zehcnasw</password>
-    <name>Wilfredo Sanchez</name>
-    <email-address>wsanchez at example.com</email-address>
-  </user>
-  <user>
-    <uid>cdaboo</uid>
+    <full-name>Wilfredo Sanchez</full-name>
+    <email>wsanchez at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>cdaboo</short-name>
+    <uid>5A985493-EE2C-4665-94CF-4DFEA3A89500</uid>
     <guid>5A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
     <password>oobadc</password>
-    <name>Cyrus Daboo</name>
-    <email-address>cdaboo at example.com</email-address>
-  </user>
-  <user>
-    <uid>lecroy</uid>
-    <guid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</guid>
+    <full-name>Cyrus Daboo</full-name>
+    <email>cdaboo at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>lecroy</short-name>
+    <uid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</uid>
     <password>yorcel</password>
-    <name>Chris Lecroy</name>
-    <email-address>lecroy at example.com</email-address>
-  </user>
-  <user>
-    <uid>dreid</uid>
-    <guid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <full-name>Chris Lecroy</full-name>
+    <email>lecroy at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>dreid</short-name>
+    <uid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</uid>
     <password>dierd</password>
-    <name>David Reid</name>
-    <email-address>dreid at example.com</email-address>
-  </user>
-  <user>
-    <uid>doublequotes</uid>
+    <full-name>David Reid</full-name>
+    <email>dreid at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>doublequotes</short-name>
+    <uid>8E04787E-336D-41ED-A70B-D233AD0DCE6F</uid>
     <guid>8E04787E-336D-41ED-A70B-D233AD0DCE6F</guid>
     <password>setouqelbuod</password>
-    <name>Double "quotey" Quotes</name>
-    <email-address>doublequotes at example.com</email-address>
-  </user>
-  <user>
-    <uid>nocalendar</uid>
-    <guid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</guid>
+    <full-name>Double "quotey" Quotes</full-name>
+    <email>doublequotes at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>purge1</short-name>
+    <uid>C76DB741-5A2A-4239-8112-10CF152AFCA4</uid>
+    <guid>C76DB741-5A2A-4239-8112-10CF152AFCA4</guid>
+    <password>purge1</password>
+    <full-name>purge1</full-name>
+    <email>purge1 at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>purge2</short-name>
+    <uid>FFED7B62-2E08-496E-BD32-B2F95FFDDB6B</uid>
+    <guid>FFED7B62-2E08-496E-BD32-B2F95FFDDB6B</guid>
+    <password>purge2</password>
+    <full-name>purge2</full-name>
+    <email>purge2 at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>home1</short-name>
+    <uid>home1</uid>
+    <password>home1</password>
+    <full-name>Home One</full-name>
+    <email>home1 at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>home2</short-name>
+    <uid>home2</uid>
+    <password>home2</password>
+    <full-name>Home Two</full-name>
+    <email>home2 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>nocalendar</short-name>
+    <uid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</uid>
     <password>radnelacon</password>
-    <name>No Calendar</name>
-    <email-address>nocalendar at example.com</email-address>
-  </user>
-  <user>
-    <uid>usera</uid>
-    <guid>7423F94A-6B76-4A3A-815B-D52CFD77935D</guid>
+    <full-name>No Calendar</full-name>
+    <email>nocalendar at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>usera</short-name>
+    <uid>7423F94A-6B76-4A3A-815B-D52CFD77935D</uid>
     <password>a</password>
-    <name>a</name>
-    <email-address>a at example.com</email-address>
-  </user>
-  <user>
-    <uid>userb</uid>
-    <guid>8A985493-EE2C-4665-94CF-4DFEA3A89500</guid>
+    <full-name>a</full-name>
+    <email>a at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>userb</short-name>
+    <uid>8A985493-EE2C-4665-94CF-4DFEA3A89500</uid>
     <password>b</password>
-    <name>b</name>
-    <email-address>b at example.com</email-address>
-  </user>
-  <user>
-    <uid>userc</uid>
-    <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2</guid>
+    <full-name>b</full-name>
+    <email>b at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>userc</short-name>
+    <uid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2</uid>
     <password>c</password>
-    <name>c</name>
-    <email-address>c at example.com</email-address>
-  </user>
-  <user>
-    <uid>usercalonly</uid>
-    <guid>9E1FFAC4-3CCD-45A1-8272-D161C92D2EEE</guid>
+    <full-name>c</full-name>
+    <email>c at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>usercalonly</short-name>
+    <uid>9E1FFAC4-3CCD-45A1-8272-D161C92D2EEE</uid>
     <password>a</password>
-    <name>a calonly</name>
-    <email-address>a-calonly at example.com</email-address>
-  </user>
-  <user>
-    <uid>useradbkonly</uid>
-    <guid>7678EC8A-A069-4E82-9066-7279C6718507</guid>
+    <full-name>a calonly</full-name>
+    <email>a-calonly at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>useradbkonly</short-name>
+    <uid>7678EC8A-A069-4E82-9066-7279C6718507</uid>
     <password>a</password>
-    <name>a adbkonly</name>
-    <email-address>a-adbkonly at example.com</email-address>
-  </user>
-  <user>
-    <uid>nonascii</uid>
-    <uid>nonascii佐藤</uid>
-    <guid>320B73A1-46E2-4180-9563-782DFDBE1F63</guid>
+    <full-name>a adbkonly</full-name>
+    <email>a-adbkonly at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>nonascii</short-name>
+    <short-name>nonascii佐藤</short-name>
+    <uid>320B73A1-46E2-4180-9563-782DFDBE1F63</uid>
     <password>a</password>
-    <name>佐藤佐藤佐藤</name>
-    <email-address>nonascii at example.com</email-address>
-  </user>
-  <user>
-    <uid>delegator</uid>
-    <guid>FC465590-E9E9-4746-ACE8-6C756A49FE4D</guid>
+    <full-name>佐藤佐藤佐藤</full-name>
+    <email>nonascii at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>delegator</short-name>
+    <uid>FC465590-E9E9-4746-ACE8-6C756A49FE4D</uid>
     <password>a</password>
-    <name>Calendar Delegator</name>
-    <email-address>calendardelegator at example.com</email-address>
-  </user>
-  <user>
-    <uid>occasionaldelegate</uid>
-    <guid>EC465590-E9E9-4746-ACE8-6C756A49FE4D</guid>
+    <full-name>Calendar Delegator</full-name>
+    <email>calendardelegator at example.com</email>
+  </record>
+  <record type="user">
+    <short-name>occasionaldelegate</short-name>
+    <uid>EC465590-E9E9-4746-ACE8-6C756A49FE4D</uid>
     <password>a</password>
-    <name>Occasional Delegate</name>
-    <email-address>occasional at example.com</email-address>
-  </user>
-    <user>
-    <uid>delegateviagroup</uid>
-    <guid>46D9D716-CBEE-490F-907A-66FA6C3767FF</guid>
+    <full-name>Occasional Delegate</full-name>
+    <email>occasional at example.com</email>
+  </record>
+    <record type="user">
+    <short-name>delegateviagroup</short-name>
+    <uid>46D9D716-CBEE-490F-907A-66FA6C3767FF</uid>
     <password>a</password>
-    <name>Delegate Via Group</name>
-    <email-address>delegateviagroup at example.com</email-address>
-  </user>
-  <group>
-    <uid>delegategroup</uid>
-    <guid>00599DAF-3E75-42DD-9DB7-52617E79943F</guid>
-    <name>Delegate Group</name>
-    <members>
-      <member type="users">delegateviagroup</member>
-    </members>
-  </group>
-  <user repeat="100">
-    <uid>user%02d</uid>
-    <guid>user%02d</guid>
-    <password>%02duser</password>
-    <name>~35 User %02d</name>
-    <first-name>~5</first-name>
-    <last-name>~9 User %02d</last-name>
-    <email-address>~10 at example.com</email-address>
-  </user>
-  <group>
-    <uid>managers</uid>
-    <guid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</guid>
+    <full-name>Delegate Via Group</full-name>
+    <email>delegateviagroup at example.com</email>
+  </record>
+  <record type="group">
+    <short-name>delegategroup</short-name>
+    <uid>00599DAF-3E75-42DD-9DB7-52617E79943F</uid>
+    <full-name>Delegate Group</full-name>
+    <member-uid>46D9D716-CBEE-490F-907A-66FA6C3767FF</member-uid>
+  </record>
+
+  <record type="user">
+    <short-name>user01</short-name>
+    <uid>user01</uid>
+    <password>user01</password>
+    <full-name>User 01</full-name>
+    <email>user01 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user02</short-name>
+    <uid>user02</uid>
+    <password>user02</password>
+    <full-name>User 02</full-name>
+    <email>user02 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user03</short-name>
+    <uid>user03</uid>
+    <password>user03</password>
+    <full-name>User 03</full-name>
+    <email>user03 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user04</short-name>
+    <uid>user04</uid>
+    <password>user04</password>
+    <full-name>User 04</full-name>
+    <email>user04 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user05</short-name>
+    <uid>user05</uid>
+    <password>user05</password>
+    <full-name>User 05</full-name>
+    <email>user05 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user06</short-name>
+    <uid>user06</uid>
+    <password>user06</password>
+    <full-name>User 06</full-name>
+    <email>user06 at example.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__wsanchez1__</uid>
+    <short-name>wsanchez1</short-name>
+    <short-name>wilfredo_sanchez</short-name>
+    <full-name>Wilfredo Sanchez</full-name>
+    <password>zehcnasw</password>
+    <email>wsanchez at bitbucket.calendarserver.org</email>
+    <email>wsanchez at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__glyph1__</uid>
+    <short-name>glyph1</short-name>
+    <full-name>Glyph Lefkowitz</full-name>
+    <password>hpylg</password>
+    <email>glyph at bitbucket.calendarserver.org</email>
+    <email>glyph at devnull.twistedmatrix.com</email>
+  </record>
+
+  <record type="user">
+    <uid>__sagen1__</uid>
+    <short-name>sagen</short-name>
+    <short-name>sagen1</short-name>
+    <full-name>Morgen Sagen</full-name>
+    <password>negas</password>
+    <email>sagen at bitbucket.calendarserver.org</email>
+  </record>
+
+  <record type="user">
+    <uid>__cdaboo1__</uid>
+    <short-name>cdaboo1</short-name>
+    <full-name>Cyrus Daboo</full-name>
+    <password>suryc</password>
+    <email>cdaboo at bitbucket.calendarserver.org</email>
+  </record>
+
+  <record type="user">
+    <uid>__dre1__</uid>
+    <short-name>dre1</short-name>
+    <short-name>dre</short-name>
+    <full-name>Andre LaBranche</full-name>
+    <password>erd</password>
+    <email>dre at bitbucket.calendarserver.org</email>
+  </record>
+
+  <record type="group">
+    <uid>__top_group_1__</uid>
+    <short-name>top-group-1</short-name>
+    <full-name>Top Group 1</full-name>
+    <email>topgroup1 at example.com</email>
+    <member-uid>__wsanchez1__</member-uid>
+    <member-uid>__glyph1__</member-uid>
+    <member-uid>__sub_group_1__</member-uid>
+  </record>
+
+  <record type="group">
+    <uid>__sub_group_1__</uid>
+    <short-name>sub-group-1</short-name>
+    <full-name>Sub Group 1</full-name>
+    <email>subgroup1 at example.com</email>
+    <member-uid>__sagen1__</member-uid>
+    <member-uid>__cdaboo1__</member-uid>
+  </record>
+
+
+  <record type="group">
+    <short-name>managers</short-name>
+    <uid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</uid>
     <password>managers</password>
-    <name>Managers</name>
-    <members>
-      <member type="users">lecroy</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Managers</full-name>
+      <member-uid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</member-uid>
+  </record>
+  <record type="group">
+    <short-name>admin</short-name>
     <uid>admin</uid>
-    <guid>admin</guid>
     <password>admin</password>
-    <name>Administrators</name>
-    <members>
-      <member type="groups">managers</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Administrators</full-name>
+      <member-uid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</member-uid>
+  </record>
+  <record type="group">
+    <short-name>grunts</short-name>
     <uid>grunts</uid>
-    <guid>grunts</guid>
     <password>grunts</password>
-    <name>We do all the work</name>
-    <members>
-      <member>wsanchez</member>
-      <member>cdaboo</member>
-      <member>dreid</member>
-    </members>
-  </group>
-  <group>
+    <full-name>We do all the work</full-name>
+      <member-uid>6423F94A-6B76-4A3A-815B-D52CFD77935D</member-uid>
+      <member-uid>5A985493-EE2C-4665-94CF-4DFEA3A89500</member-uid>
+      <member-uid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</member-uid>
+  </record>
+  <record type="group">
+    <short-name>right_coast</short-name>
     <uid>right_coast</uid>
-    <guid>right_coast</guid>
     <password>right_coast</password>
-    <name>East Coast</name>
-    <members>
-      <member>cdaboo</member>
-    </members>
-  </group>
-  <group>
+    <full-name>East Coast</full-name>
+      <member-uid>5A985493-EE2C-4665-94CF-4DFEA3A89500</member-uid>
+  </record>
+  <record type="group">
+    <short-name>left_coast</short-name>
     <uid>left_coast</uid>
-    <guid>left_coast</guid>
     <password>left_coast</password>
-    <name>West Coast</name>
-    <members>
-      <member>wsanchez</member>
-      <member>lecroy</member>
-      <member>dreid</member>
-    </members>
-  </group>
-  <group>
+    <full-name>West Coast</full-name>
+      <member-uid>6423F94A-6B76-4A3A-815B-D52CFD77935D</member-uid>
+      <member-uid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</member-uid>
+      <member-uid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</member-uid>
+  </record>
+  <record type="group">
+    <short-name>both_coasts</short-name>
     <uid>both_coasts</uid>
-    <guid>both_coasts</guid>
     <password>both_coasts</password>
-    <name>Both Coasts</name>
-    <members>
-      <member type="groups">right_coast</member>
-      <member type="groups">left_coast</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Both Coasts</full-name>
+      <member-uid>right_coast</member-uid>
+      <member-uid>left_coast</member-uid>
+  </record>
+  <record type="group">
+    <short-name>recursive1_coasts</short-name>
     <uid>recursive1_coasts</uid>
-    <guid>recursive1_coasts</guid>
     <password>recursive1_coasts</password>
-    <name>Recursive1 Coasts</name>
-    <members>
-      <member type="groups">recursive2_coasts</member>
-      <member>wsanchez</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Recursive1 Coasts</full-name>
+      <member-uid>recursive2_coasts</member-uid>
+      <member-uid>6423F94A-6B76-4A3A-815B-D52CFD77935D</member-uid>
+  </record>
+  <record type="group">
+    <short-name>recursive2_coasts</short-name>
     <uid>recursive2_coasts</uid>
-    <guid>recursive2_coasts</guid>
     <password>recursive2_coasts</password>
-    <name>Recursive2 Coasts</name>
-    <members>
-      <member type="groups">recursive1_coasts</member>
-      <member>cdaboo</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Recursive2 Coasts</full-name>
+      <member-uid>recursive1_coasts</member-uid>
+      <member-uid>5A985493-EE2C-4665-94CF-4DFEA3A89500</member-uid>
+  </record>
+  <record type="group">
+    <short-name>non_calendar_group</short-name>
     <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>
-  </group>
-  <location>
-    <uid>mercury</uid>
-    <guid>mercury</guid>
-    <password>mercury</password>
-    <name>Mercury Seven</name>
-    <email-address>mercury at example.com</email-address>
-  </location>
-  <location>
-    <uid>gemini</uid>
-    <guid>gemini</guid>
-    <password>gemini</password>
-    <name>Gemini Twelve</name>
-    <email-address>gemini at example.com</email-address>
-  </location>
-  <location>
-    <uid>apollo</uid>
-    <guid>apollo</guid>
-    <password>apollo</password>
-    <name>Apollo Eleven</name>
-    <email-address>apollo at example.com</email-address>
-  </location>
-  <location>
-    <uid>orion</uid>
-    <guid>orion</guid>
-    <password>orion</password>
-    <name>Orion</name>
-    <email-address>orion at example.com</email-address>
-  </location>
-  <resource>
-    <uid>transporter</uid>
-    <guid>transporter</guid>
-    <password>transporter</password>
-    <name>Mass Transporter</name>
-    <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>
-    <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>
-    <email-address>non_calendar_proxy at example.com</email-address>
-  </resource>
-  <resource>
-    <uid>disabled</uid>
-    <guid>disabled</guid>
-    <password>disabled</password>
-    <name>Disabled Record</name>
-    <email-address>disabled at example.com</email-address>
-  </resource>
-</accounts>
+    <full-name>Non-calendar group</full-name>
+      <member-uid>5A985493-EE2C-4665-94CF-4DFEA3A89500</member-uid>
+      <member-uid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</member-uid>
+  </record>
+
+  <!-- Calverify test records -->
+
+  <record type="user">
+    <short-name>example1</short-name>
+    <uid>D46F3D71-04B7-43C2-A7B6-6F92F92E61D0</uid>
+    <guid>D46F3D71-04B7-43C2-A7B6-6F92F92E61D0</guid>
+    <password>example</password>
+    <full-name>Example User1</full-name>
+    <email>example1 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>example2</short-name>
+    <uid>47B16BB4-DB5F-4BF6-85FE-A7DA54230F92</uid>
+    <guid>47B16BB4-DB5F-4BF6-85FE-A7DA54230F92</guid>
+    <password>example</password>
+    <full-name>Example User2</full-name>
+    <email>example2 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>example3</short-name>
+    <uid>AC478592-7783-44D1-B2AE-52359B4E8415</uid>
+    <guid>AC478592-7783-44D1-B2AE-52359B4E8415</guid>
+    <password>example</password>
+    <full-name>Example User3</full-name>
+    <email>example3 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>example4</short-name>
+    <uid>A89E3A97-1658-4E45-A185-479F3E49D446</uid>
+    <guid>A89E3A97-1658-4E45-A185-479F3E49D446</guid>
+    <password>example</password>
+    <full-name>Example User4</full-name>
+    <email>example4 at example.com</email>
+  </record>
+
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/augments.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/augments.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/augments.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,177 +19,144 @@
 <!DOCTYPE augments SYSTEM "../../../conf/auth/augments.dtd">
 
 <augments realm="Test">
+  <!--
   <record>
+    <uid>Location-Default</uid>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>Resource-Default</uid>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+  </record>
+  -->
+
+  <record>
     <uid>D11F03A0-97EA-48AF-9A6C-FAC7F3975766</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>6423F94A-6B76-4A3A-815B-D52CFD77935D</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
     <server-id>00001</server-id>
   </record>
   <record>
     <uid>5A985493-EE2C-4665-94CF-4DFEA3A89500</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
     <server-id>00002</server-id>
   </record>
   <record>
     <uid>8B4288F6-CC82-491D-8EF9-642EF4F3E7D0</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>543D28BA-F74F-4D5F-9243-B3E3A61171E5</uid>
-    <enable>true</enable>
     <enable-calendar>false</enable-calendar>
     <enable-addressbook>false</enable-addressbook>
   </record>
   <record repeat="100">
     <uid>user%02d</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
-    <uid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1</uid>
-    <enable>true</enable>
-  </record>
-  <record>
-    <uid>admin</uid>
-    <enable>true</enable>
-  </record>
-  <record>
-    <uid>grunts</uid>
-    <enable>true</enable>
-  </record>
-  <record>
     <uid>right_coast</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>left_coast</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
-    <uid>both_coasts</uid>
-    <enable>true</enable>
-  </record>
-  <record>
-    <uid>recursive1_coasts</uid>
-    <enable>true</enable>
-  </record>
-  <record>
-    <uid>recursive2_coasts</uid>
-    <enable>true</enable>
-  </record>
-  <record>
-    <uid>non_calendar_group</uid>
-    <enable>true</enable>
-  </record>
-  <record>
     <uid>mercury</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>gemini</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>apollo</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
     <auto-accept-group>both_coasts</auto-accept-group>
   </record>
   <record>
     <uid>orion</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>transporter</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>ftlcpu</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
+  <!--
   <record>
     <uid>non_calendar_proxy</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-addressbook>true</enable-addressbook>
   </record>
+-->
   <record>
-    <uid>disabled</uid>
-    <enable>false</enable>
-  </record>
-  <record>
     <uid>7423F94A-6B76-4A3A-815B-D52CFD77935D</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
   </record>
   <record>
     <uid>8A985493-EE2C-4665-94CF-4DFEA3A89500</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
   </record>
   <record>
     <uid>9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
   </record>
   <record>
     <uid>9E1FFAC4-3CCD-45A1-8272-D161C92D2EEE</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
   </record>
   <record>
     <uid>7678EC8A-A069-4E82-9066-7279C6718507</uid>
-    <enable>true</enable>
     <enable-addressbook>true</enable-addressbook>
   </record>
   <record>
     <uid>FC465590-E9E9-4746-ACE8-6C756A49FE4D</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
   </record>
   <record>
     <uid>EC465590-E9E9-4746-ACE8-6C756A49FE4D</uid>
-    <enable>true</enable>
     <enable-calendar>true</enable-calendar>
     <enable-login>true</enable-login>
   </record>
   <record>
     <uid>00599DAF-3E75-42DD-9DB7-52617E79943F</uid>
-    <enable>true</enable>
     <enable-calendar>false</enable-calendar>
     <enable-login>false</enable-login>
   </record>
+  <record>
+    <uid>75EA36BE-F71B-40F9-81F9-CF59BF40CA8F</uid>
+    <enable-calendar>true</enable-calendar>
+    <auto-schedule>true</auto-schedule>
+  </record>
+
 </augments>

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/resources.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/resources.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/resources.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,5 +18,103 @@
 
 <!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
 
-<accounts realm="Test">
-</accounts>
+<directory realm="Test">
+  <record type="location">
+    <uid>mercury</uid>
+    <short-name>mercury</short-name>
+    <password>mercury</password>
+    <full-name>Mercury Seven</full-name>
+    <email>mercury at example.com</email>
+  </record>
+  <record type="location">
+    <uid>gemini</uid>
+    <short-name>gemini</short-name>
+    <password>gemini</password>
+    <full-name>Gemini Twelve</full-name>
+    <email>gemini at example.com</email>
+  </record>
+  <record type="location">
+    <uid>apollo</uid>
+    <short-name>apollo</short-name>
+    <password>apollo</password>
+    <full-name>Apollo Eleven</full-name>
+    <email>apollo at example.com</email>
+  </record>
+  <record type="location">
+    <uid>orion</uid>
+    <short-name>orion</short-name>
+    <password>orion</password>
+    <full-name>Orion</full-name>
+    <email>orion at example.com</email>
+  </record>
+  <record type="resource">
+    <uid>transporter</uid>
+    <short-name>transporter</short-name>
+    <password>transporter</password>
+    <full-name>Mass Transporter</full-name>
+    <email>transporter at example.com</email>
+  </record>
+  <record type="resource">
+    <uid>ftlcpu</uid>
+    <short-name>ftlcpu</short-name>
+    <password>ftlcpu</password>
+    <full-name>Faster-Than-Light Microprocessor</full-name>
+    <email>ftlcpu at example.com</email>
+  </record>
+  <record type="resource">
+    <uid>non_calendar_proxy</uid>
+    <short-name>non_calendar_proxy</short-name>
+    <password>non_calendar_proxy</password>
+    <full-name>Non-calendar proxy</full-name>
+    <email>non_calendar_proxy at example.com</email>
+  </record>
+  <record type="resource">
+    <uid>disabled</uid>
+    <short-name>disabled</short-name>
+    <password>disabled</password>
+    <full-name>Disabled Record</full-name>
+    <email>disabled at example.com</email>
+  </record>
+  <record type="location">
+    <uid>__sanchezoffice__</uid>
+    <short-name>sanchezoffice</short-name>
+    <full-name>Sanchez Office</full-name>
+  </record>
+  <record type="location">
+    <short-name>location01</short-name>
+    <uid>75EA36BE-F71B-40F9-81F9-CF59BF40CA8F</uid>
+    <guid>75EA36BE-F71B-40F9-81F9-CF59BF40CA8F</guid>
+    <password>location01</password>
+    <full-name>Room 01</full-name>
+  </record>
+  <record type="location">
+    <short-name>room-with-address-1</short-name>
+    <uid>room-addr-1</uid>
+    <guid>634A102B-6902-464F-9451-8A86A31628C1</guid>
+    <password>room-addr-2</password>
+    <full-name>Room with Address 1</full-name>
+    <associated-address>1-infinite-loop</associated-address>
+  </record>
+  <record type="location">
+    <short-name>room-with-address-2</short-name>
+    <uid>room-addr-2</uid>
+    <password>room-addr-2</password>
+    <full-name>Room with Address 2</full-name>
+    <associated-address>2-infinite-loop</associated-address>
+  </record>
+  <record type="address">
+    <short-name>il1</short-name>
+    <uid>1-infinite-loop</uid>
+    <full-name>One Infinite Loop</full-name>
+    <street-address>1 Infinite Loop, Cupertino, CA 95014</street-address>
+    <geographic-location>37.331741,-122.030333</geographic-location>
+  </record>
+  <record type="address">
+    <short-name>il2</short-name>
+    <uid>2-infinite-loop</uid>
+    <full-name>Two Infinite Loop</full-name>
+    <street-address>2 Infinite Loop, Cupertino, CA 95014</street-address>
+    <geographic-location>37.332633,-122.030502</geographic-location>
+  </record>
+
+</directory>

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_aggregate.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_aggregate.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_aggregate.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,87 +0,0 @@
-##
-# Copyright (c) 2005-2014 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.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.aggregate import AggregateDirectoryService
-
-from twistedcaldav.directory.test.test_xmlfile import xmlFile, augmentsFile
-
-import twistedcaldav.directory.test.util
-from twistedcaldav.directory import augment
-
-xml_prefix = "xml:"
-
-testServices = (
-    (xml_prefix   , twistedcaldav.directory.test.test_xmlfile.XMLFile),
-)
-
-class AggregatedDirectories (twistedcaldav.directory.test.util.DirectoryTestCase):
-    def _recordTypes(self):
-        recordTypes = set()
-        for prefix, testClass in testServices:
-            for recordType in testClass.recordTypes:
-                recordTypes.add(prefix + recordType)
-        return recordTypes
-
-
-    def _records(key): #@NoSelf
-        def get(self):
-            records = {}
-            for prefix, testClass in testServices:
-                for record, info in getattr(testClass, key).iteritems():
-                    info = dict(info)
-                    info["prefix"] = prefix
-                    info["members"] = tuple(
-                        (t, prefix + s) for t, s in info.get("members", {})
-                    )
-                    records[prefix + record] = info
-            return records
-        return get
-
-    recordTypes = property(_recordTypes)
-    users = property(_records("users"))
-    groups = property(_records("groups"))
-    locations = property(_records("locations"))
-    resources = property(_records("resources"))
-    addresses = property(_records("addresses"))
-
-    recordTypePrefixes = tuple(s[0] for s in testServices)
-
-
-    def service(self):
-        """
-        Returns an IDirectoryService.
-        """
-        xmlService = XMLDirectoryService(
-            {
-                'xmlFile' : xmlFile,
-                'augmentService' :
-                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-            }
-        )
-        xmlService.recordTypePrefix = xml_prefix
-
-        return AggregateDirectoryService((xmlService,), None)
-
-
-    def test_setRealm(self):
-        """
-        setRealm gets propagated to nested services
-        """
-        aggregatedService = self.service()
-        aggregatedService.setRealm("foo.example.com")
-        for service in aggregatedService._recordTypes.values():
-            self.assertEquals("foo.example.com", service.realmName)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_augment.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_augment.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_augment.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -17,7 +17,6 @@
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.directory.augment import AugmentXMLDB, AugmentSqliteDB, \
     AugmentPostgreSQLDB, AugmentRecord
-from twistedcaldav.directory.directory import DirectoryService
 from twisted.internet.defer import inlineCallbacks
 from twistedcaldav.directory.xmlaugmentsparser import XMLAugmentsParser
 import cStringIO
@@ -78,7 +77,7 @@
 class AugmentTests(TestCase):
 
     @inlineCallbacks
-    def _checkRecord(self, db, items, recordType=DirectoryService.recordType_users):
+    def _checkRecord(self, db, items, recordType="users"):
 
         record = (yield db.getAugmentRecord(items["uid"], recordType))
         self.assertTrue(record is not None, "Failed record uid: %s" % (items["uid"],))
@@ -88,7 +87,7 @@
 
 
     @inlineCallbacks
-    def _checkRecordExists(self, db, uid, recordType=DirectoryService.recordType_users):
+    def _checkRecordExists(self, db, uid, recordType="users"):
 
         record = (yield db.getAugmentRecord(uid, recordType))
         self.assertTrue(record is not None, "Failed record uid: %s" % (uid,))

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_buildquery.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_buildquery.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_buildquery.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,160 +0,0 @@
-##
-# Copyright (c) 2009-2014 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 calendarserver.platform.darwin.od import dsattributes
-except ImportError:
-    pass
-else:
-    from twistedcaldav.test.util import TestCase
-    from twistedcaldav.directory.appleopendirectory import (buildQueries,
-        buildLocalQueriesFromTokens, OpenDirectoryService, buildNestedQueryFromTokens)
-
-    class BuildQueryTests(TestCase):
-
-        def test_buildQuery(self):
-            self.assertEquals(
-                buildQueries(
-                    [dsattributes.kDSStdRecordTypeUsers],
-                    (
-                        ("firstName", "morgen", True, "starts-with"),
-                        ("lastName", "sagen", True, "starts-with"),
-                    ),
-                    OpenDirectoryService._ODFields
-                ),
-                {
-                    ('dsAttrTypeStandard:FirstName', 'morgen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeUsers],
-                    ('dsAttrTypeStandard:LastName', 'sagen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeUsers],
-                }
-            )
-            self.assertEquals(
-                buildQueries(
-                    [
-                        dsattributes.kDSStdRecordTypeUsers,
-                    ],
-                    (
-                        ("firstName", "morgen", True, "starts-with"),
-                        ("emailAddresses", "morgen", True, "contains"),
-                    ),
-                    OpenDirectoryService._ODFields
-                ),
-                {
-                    ('dsAttrTypeStandard:FirstName', 'morgen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeUsers],
-                    ('dsAttrTypeStandard:EMailAddress', 'morgen', True, 'contains') : [dsattributes.kDSStdRecordTypeUsers],
-                }
-            )
-            self.assertEquals(
-                buildQueries(
-                    [
-                        dsattributes.kDSStdRecordTypeGroups,
-                    ],
-                    (
-                        ("firstName", "morgen", True, "starts-with"),
-                        ("lastName", "morgen", True, "starts-with"),
-                        ("fullName", "morgen", True, "starts-with"),
-                        ("emailAddresses", "morgen", True, "contains"),
-                    ),
-                    OpenDirectoryService._ODFields
-                ),
-                {
-                    ('dsAttrTypeStandard:RealName', 'morgen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeGroups],
-                    ('dsAttrTypeStandard:EMailAddress', 'morgen', True, 'contains') : [dsattributes.kDSStdRecordTypeGroups],
-                }
-            )
-            self.assertEquals(
-                buildQueries(
-                    [
-                        dsattributes.kDSStdRecordTypeUsers,
-                        dsattributes.kDSStdRecordTypeGroups,
-                    ],
-                    (
-                        ("firstName", "morgen", True, "starts-with"),
-                        ("lastName", "morgen", True, "starts-with"),
-                        ("fullName", "morgen", True, "starts-with"),
-                        ("emailAddresses", "morgen", True, "contains"),
-                    ),
-                    OpenDirectoryService._ODFields
-                ),
-                {
-                    ('dsAttrTypeStandard:RealName', 'morgen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeUsers, dsattributes.kDSStdRecordTypeGroups],
-                    ('dsAttrTypeStandard:EMailAddress', 'morgen', True, 'contains') : [dsattributes.kDSStdRecordTypeUsers, dsattributes.kDSStdRecordTypeGroups],
-                    ('dsAttrTypeStandard:FirstName', 'morgen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeUsers],
-                    ('dsAttrTypeStandard:LastName', 'morgen', True, 'starts-with') : [dsattributes.kDSStdRecordTypeUsers],
-                }
-            )
-            self.assertEquals(
-                buildQueries(
-                    [
-                        dsattributes.kDSStdRecordTypeGroups,
-                    ],
-                    (
-                        ("firstName", "morgen", True, "starts-with"),
-                    ),
-                    OpenDirectoryService._ODFields
-                ),
-                {
-                }
-            )
-
-
-        def test_buildLocalQueryFromTokens(self):
-            """
-            Verify the generating of the simpler queries passed to /Local/Default
-            """
-            results = buildLocalQueriesFromTokens([], OpenDirectoryService._ODFields)
-            self.assertEquals(results, None)
-
-            results = buildLocalQueriesFromTokens(["foo"], OpenDirectoryService._ODFields)
-            self.assertEquals(
-                results[0].generate(),
-                "(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=foo*))"
-            )
-
-            results = buildLocalQueriesFromTokens(["foo", "bar"], OpenDirectoryService._ODFields)
-            self.assertEquals(
-                results[0].generate(),
-                "(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=foo*))"
-            )
-            self.assertEquals(
-                results[1].generate(),
-                "(|(dsAttrTypeStandard:RealName=*bar*)(dsAttrTypeStandard:EMailAddress=bar*))"
-            )
-
-
-        def test_buildNestedQueryFromTokens(self):
-            """
-            Verify the generating of the complex nested queries
-            """
-            query = buildNestedQueryFromTokens([], OpenDirectoryService._ODFields)
-            self.assertEquals(query, None)
-
-            query = buildNestedQueryFromTokens(["foo"], OpenDirectoryService._ODFields)
-            self.assertEquals(
-                query.generate(),
-                "(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=foo*)(dsAttrTypeStandard:RecordName=foo*))"
-            )
-
-            query = buildNestedQueryFromTokens(["foo", "bar"], OpenDirectoryService._ODFields)
-            self.assertEquals(
-                query.generate(),
-                "(&(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=foo*)(dsAttrTypeStandard:RecordName=foo*))(|(dsAttrTypeStandard:RealName=*bar*)(dsAttrTypeStandard:EMailAddress=bar*)(dsAttrTypeStandard:RecordName=bar*)))"
-            )
-
-            query = buildNestedQueryFromTokens(["foo", "bar", "baz"], OpenDirectoryService._ODFields)
-            self.assertEquals(
-                query.generate(),
-                "(&(|(dsAttrTypeStandard:RealName=*foo*)(dsAttrTypeStandard:EMailAddress=foo*)(dsAttrTypeStandard:RecordName=foo*))(|(dsAttrTypeStandard:RealName=*bar*)(dsAttrTypeStandard:EMailAddress=bar*)(dsAttrTypeStandard:RecordName=bar*))(|(dsAttrTypeStandard:RealName=*baz*)(dsAttrTypeStandard:EMailAddress=baz*)(dsAttrTypeStandard:RecordName=baz*)))"
-            )

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_cachedirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_cachedirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_cachedirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,405 +0,0 @@
-#
-# Copyright (c) 2009-2014 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 uuid import uuid4
-
-from twistedcaldav.directory.cachingdirectory import CachingDirectoryService
-from twistedcaldav.directory.cachingdirectory import CachingDirectoryRecord
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.util import uuidFromName
-from twistedcaldav.directory.augment import AugmentRecord
-from twistedcaldav.test.util import TestCase
-from twistedcaldav.config import config
-
-
-class TestDirectoryService (CachingDirectoryService):
-
-    realmName = "Dummy Realm"
-    baseGUID = "20CB1593-DE3F-4422-A7D7-BA9C2099B317"
-
-    def recordTypes(self):
-        return (
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-            DirectoryService.recordType_locations,
-            DirectoryService.recordType_resources,
-        )
-
-
-    def queryDirectory(self, recordTypes, indexType, indexKey):
-
-        self.queried = True
-
-        for recordType in recordTypes:
-            for record in self.fakerecords[recordType]:
-                cacheIt = False
-                if indexType in (
-                    CachingDirectoryService.INDEX_TYPE_SHORTNAME,
-                    CachingDirectoryService.INDEX_TYPE_CUA,
-                    CachingDirectoryService.INDEX_TYPE_AUTHID,
-                ):
-                    if indexKey in record[indexType]:
-                        cacheIt = True
-                else:
-                    if indexKey == record[indexType]:
-                        cacheIt = True
-
-                if cacheIt:
-                    cacheRecord = CachingDirectoryRecord(
-                        service=self,
-                        recordType=recordType,
-                        guid=record.get("guid"),
-                        shortNames=record.get("shortname"),
-                        authIDs=record.get("authid"),
-                        fullName=record.get("fullName"),
-                        firstName="",
-                        lastName="",
-                        emailAddresses=record.get("email"),
-                    )
-
-                    augmentRecord = AugmentRecord(
-                        uid=cacheRecord.guid,
-                        enabled=True,
-                        enabledForCalendaring=True,
-                    )
-
-                    cacheRecord.addAugmentInformation(augmentRecord)
-
-                    self.recordCacheForType(recordType).addRecord(cacheRecord,
-                        indexType, indexKey)
-
-
-
-class CachingDirectoryTest(TestCase):
-
-    baseGUID = str(uuid4())
-
-
-    def setUp(self):
-        super(CachingDirectoryTest, self).setUp()
-        self.service = TestDirectoryService()
-        self.service.queried = False
-
-
-    def loadRecords(self, records):
-        self.service._initCaches()
-        self.service.fakerecords = records
-        self.service.queried = False
-
-
-    def fakeRecord(
-        self,
-        fullName,
-        recordType,
-        shortNames=None,
-        guid=None,
-        emails=None,
-        members=None,
-        resourceInfo=None,
-        multinames=False
-    ):
-        if shortNames is None:
-            shortNames = (self.shortNameForFullName(fullName),)
-            if multinames:
-                shortNames += (fullName,)
-
-        if guid is None:
-            guid = self.guidForShortName(shortNames[0], recordType=recordType)
-        else:
-            guid = guid.lower()
-
-        if emails is None:
-            emails = ("%s at example.com" % (shortNames[0],),)
-
-        attrs = {
-            "fullName": fullName,
-            "guid": guid,
-            "shortname": shortNames,
-            "email": emails,
-            "cua": tuple(["mailto:%s" % email for email in emails]),
-            "authid": tuple(["Kerberos:%s" % email for email in emails])
-        }
-
-        if members:
-            attrs["members"] = members
-
-        if resourceInfo:
-            attrs["resourceInfo"] = resourceInfo
-
-        return attrs
-
-
-    def shortNameForFullName(self, fullName):
-        return fullName.lower().replace(" ", "")
-
-
-    def guidForShortName(self, shortName, recordType=""):
-        return uuidFromName(self.baseGUID, "%s%s" % (recordType, shortName))
-
-
-    def dummyRecords(self):
-        SIZE = 10
-        records = {
-            DirectoryService.recordType_users: [
-                self.fakeRecord("User %02d" % x, DirectoryService.recordType_users, multinames=(x > 5)) for x in range(1, SIZE + 1)
-            ],
-            DirectoryService.recordType_groups: [
-                self.fakeRecord("Group %02d" % x, DirectoryService.recordType_groups) for x in range(1, SIZE + 1)
-            ],
-            DirectoryService.recordType_resources: [
-                self.fakeRecord("Resource %02d" % x, DirectoryService.recordType_resources) for x in range(1, SIZE + 1)
-            ],
-            DirectoryService.recordType_locations: [
-                self.fakeRecord("Location %02d" % x, DirectoryService.recordType_locations) for x in range(1, SIZE + 1)
-            ],
-        }
-        # Add duplicate shortnames
-        records[DirectoryService.recordType_users].append(self.fakeRecord("Duplicate", DirectoryService.recordType_users, multinames=True))
-        records[DirectoryService.recordType_groups].append(self.fakeRecord("Duplicate", DirectoryService.recordType_groups, multinames=True))
-        records[DirectoryService.recordType_resources].append(self.fakeRecord("Duplicate", DirectoryService.recordType_resources, multinames=True))
-        records[DirectoryService.recordType_locations].append(self.fakeRecord("Duplicate", DirectoryService.recordType_locations, multinames=True))
-
-        self.loadRecords(records)
-
-
-    def verifyRecords(self, recordType, expectedGUIDs):
-
-        records = self.service.listRecords(recordType)
-        recordGUIDs = set([record.guid for record in records])
-        self.assertEqual(recordGUIDs, expectedGUIDs)
-
-
-
-class GUIDLookups(CachingDirectoryTest):
-
-    def test_emptylist(self):
-        self.dummyRecords()
-
-        self.verifyRecords(DirectoryService.recordType_users, set())
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-
-    def test_cacheoneguid(self):
-        self.dummyRecords()
-
-        self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01", recordType=DirectoryService.recordType_users)) is not None)
-        self.assertTrue(self.service.queried)
-        self.verifyRecords(DirectoryService.recordType_users, set((
-            self.guidForShortName("user01", recordType=DirectoryService.recordType_users),
-        )))
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-        # Make sure it really is cached and won't cause another query
-        self.service.queried = False
-        self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01", recordType=DirectoryService.recordType_users)) is not None)
-        self.assertFalse(self.service.queried)
-
-        # Make sure guid is case-insensitive
-        self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01", recordType=DirectoryService.recordType_users).lower()) is not None)
-
-
-    def test_cacheoneshortname(self):
-        self.dummyRecords()
-
-        self.assertTrue(self.service.recordWithShortName(
-            DirectoryService.recordType_users,
-            "user02"
-        ) is not None)
-        self.assertTrue(self.service.queried)
-        self.verifyRecords(DirectoryService.recordType_users, set((
-            self.guidForShortName("user02", recordType=DirectoryService.recordType_users),
-        )))
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-        # Make sure it really is cached and won't cause another query
-        self.service.queried = False
-        self.assertTrue(self.service.recordWithShortName(
-            DirectoryService.recordType_users,
-            "user02"
-        ) is not None)
-        self.assertFalse(self.service.queried)
-
-
-    def test_cacheoneemail(self):
-        self.dummyRecords()
-
-        self.assertTrue(self.service.recordWithCalendarUserAddress(
-            "mailto:user03 at example.com"
-        ) is not None)
-        self.assertTrue(self.service.queried)
-        self.verifyRecords(DirectoryService.recordType_users, set((
-            self.guidForShortName("user03", recordType=DirectoryService.recordType_users),
-        )))
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-        # Make sure it really is cached and won't cause another query
-        self.service.queried = False
-        self.assertTrue(self.service.recordWithCalendarUserAddress(
-            "mailto:user03 at example.com"
-        ) is not None)
-        self.assertFalse(self.service.queried)
-
-
-    def test_cacheonePrincipalsURLWithUIDS(self):
-        self.dummyRecords()
-
-        guid = self.guidForShortName("user03", "users")
-        self.assertTrue(self.service.recordWithCalendarUserAddress(
-            "/principals/__uids__/%s" % (guid,)
-        ) is not None)
-        self.assertTrue(self.service.queried)
-        self.verifyRecords(DirectoryService.recordType_users, set((
-            self.guidForShortName("user03", recordType=DirectoryService.recordType_users),
-        )))
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-        # Make sure it really is cached and won't cause another query
-        self.service.queried = False
-        self.assertTrue(self.service.recordWithCalendarUserAddress(
-            "/principals/__uids__/%s" % (guid,)
-        ) is not None)
-        self.assertFalse(self.service.queried)
-
-
-    def test_cacheonePrincipalsURLWithUsers(self):
-        self.dummyRecords()
-
-        self.assertTrue(self.service.recordWithCalendarUserAddress(
-            "/principals/users/user03"
-        ) is not None)
-        self.assertTrue(self.service.queried)
-        self.verifyRecords(DirectoryService.recordType_users, set((
-            self.guidForShortName("user03", recordType=DirectoryService.recordType_users),
-        )))
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-        # Make sure it really is cached and won't cause another query
-        self.service.queried = False
-        self.assertTrue(self.service.recordWithCalendarUserAddress(
-            "/principals/users/user03"
-        ) is not None)
-        self.assertFalse(self.service.queried)
-
-
-    def test_cacheoneauthid(self):
-        self.dummyRecords()
-
-        self.assertTrue(self.service.recordWithAuthID(
-            "Kerberos:user03 at example.com"
-        ) is not None)
-        self.assertTrue(self.service.queried)
-        self.verifyRecords(DirectoryService.recordType_users, set((
-            self.guidForShortName("user03", recordType=DirectoryService.recordType_users),
-        )))
-        self.verifyRecords(DirectoryService.recordType_groups, set())
-        self.verifyRecords(DirectoryService.recordType_resources, set())
-        self.verifyRecords(DirectoryService.recordType_locations, set())
-
-        # Make sure it really is cached and won't cause another query
-        self.service.queried = False
-        self.assertTrue(self.service.recordWithAuthID(
-            "Kerberos:user03 at example.com"
-        ) is not None)
-        self.assertFalse(self.service.queried)
-
-
-    def test_negativeCaching(self):
-        self.dummyRecords()
-
-        # If negativeCaching is off, each miss will result in a call to
-        # queryDirectory( )
-        self.service.negativeCaching = False
-
-        self.service.queried = False
-        self.assertEquals(self.service.recordWithGUID(self.guidForShortName("missing")), None)
-        self.assertTrue(self.service.queried)
-
-        self.service.queried = False
-        self.assertEquals(self.service.recordWithGUID(self.guidForShortName("missing")), None)
-        self.assertTrue(self.service.queried)
-
-        # However, if negativeCaching is on, a miss is recorded as such,
-        # preventing a similar queryDirectory( ) until cacheTimeout passes
-        self.service.negativeCaching = True
-
-        self.service.queried = False
-        self.assertEquals(self.service.recordWithGUID(self.guidForShortName("missing")), None)
-        self.assertTrue(self.service.queried)
-
-        self.service.queried = False
-        self.assertEquals(self.service.recordWithGUID(self.guidForShortName("missing")), None)
-        self.assertFalse(self.service.queried)
-
-        # Simulate time passing by clearing the negative timestamp for this
-        # entry, then try again, this time queryDirectory( ) is called
-        self.service._disabledKeys[self.service.INDEX_TYPE_GUID][self.guidForShortName("missing")] = 0
-
-        self.service.queried = False
-        self.assertEquals(self.service.recordWithGUID(self.guidForShortName("missing")), None)
-        self.assertTrue(self.service.queried)
-
-
-    def test_duplicateShortNames(self):
-        """
-        Verify that when looking up records having duplicate short-names, the record of the
-        proper type is returned
-        """
-
-        self.patch(config.Memcached.Pools.Default, "ClientEnabled", True)
-        self.dummyRecords()
-
-        record = self.service.recordWithShortName(DirectoryService.recordType_users,
-            "Duplicate")
-        self.assertEquals(record.recordType, DirectoryService.recordType_users)
-
-        record = self.service.recordWithShortName(DirectoryService.recordType_groups,
-            "Duplicate")
-        self.assertEquals(record.recordType, DirectoryService.recordType_groups)
-
-        record = self.service.recordWithShortName(DirectoryService.recordType_resources,
-            "Duplicate")
-        self.assertEquals(record.recordType, DirectoryService.recordType_resources)
-
-        record = self.service.recordWithShortName(DirectoryService.recordType_locations,
-            "Duplicate")
-        self.assertEquals(record.recordType, DirectoryService.recordType_locations)
-
-
-    def test_generateMemcacheKey(self):
-        """
-        Verify keys are correctly generated based on the index type -- if index type is
-        short-name, then the recordtype is encoded into the key.
-        """
-        self.assertEquals(
-            self.service.generateMemcacheKey(self.service.INDEX_TYPE_GUID, "foo", "users"),
-            "dir|v2|20CB1593-DE3F-4422-A7D7-BA9C2099B317|guid|foo",
-        )
-        self.assertEquals(
-            self.service.generateMemcacheKey(self.service.INDEX_TYPE_SHORTNAME, "foo", "users"),
-            "dir|v2|20CB1593-DE3F-4422-A7D7-BA9C2099B317|users|shortname|foo",
-        )

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_directory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_directory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_directory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,1201 +0,0 @@
-##
-# Copyright (c) 2011-2014 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
-from twisted.python.filepath import FilePath
-
-from twistedcaldav.test.util import TestCase
-from twistedcaldav.test.util import xmlFile, augmentsFile, proxiesFile, dirTest
-from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, GroupMembershipCache, GroupMembershipCacheUpdater, diffAssignments
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
-from twistedcaldav.directory import augment, calendaruserproxy
-from twistedcaldav.directory.util import normalizeUUID
-from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
-
-import cPickle as pickle
-import uuid
-
-def StubCheckSACL(cls, username, service):
-    services = {
-        "calendar" : ["amanda", "betty"],
-        "addressbook" : ["amanda", "carlene"],
-    }
-    if username in services[service]:
-        return 0
-    return 1
-
-
-
-class SACLTests(TestCase):
-
-    def setUp(self):
-        self.patch(DirectoryRecord, "CheckSACL", StubCheckSACL)
-        self.patch(config, "EnableSACLs", True)
-        self.service = DirectoryService()
-        self.service.setRealm("test")
-        self.service.baseGUID = "0E8E6EC2-8E52-4FF3-8F62-6F398B08A498"
-
-
-    def test_applySACLs(self):
-        """
-        Users not in calendar SACL will have enabledForCalendaring set to
-        False.
-        Users not in addressbook SACL will have enabledForAddressBooks set to
-        False.
-        """
-
-        data = [
-            ("amanda", True, True,),
-            ("betty", True, False,),
-            ("carlene", False, True,),
-            ("daniel", False, False,),
-        ]
-        for username, cal, ab in data:
-            record = DirectoryRecord(self.service, "users", None, (username,),
-                enabledForCalendaring=True, enabledForAddressBooks=True)
-            record.applySACLs()
-            self.assertEquals(record.enabledForCalendaring, cal)
-            self.assertEquals(record.enabledForAddressBooks, ab)
-
-
-
-class GroupMembershipTests (TestCase):
-
-    @inlineCallbacks
-    def setUp(self):
-        super(GroupMembershipTests, self).setUp()
-
-        self.directoryFixture.addDirectoryService(XMLDirectoryService(
-            {
-                'xmlFile' : xmlFile,
-                'augmentService' :
-                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-            }
-        ))
-        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
-
-        # Set up a principals hierarchy for each service we're testing with
-        self.principalRootResources = {}
-        name = self.directoryService.__class__.__name__
-        url = "/" + name + "/"
-
-        provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
-
-        self.site.resource.putChild(name, provisioningResource)
-
-        self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
-
-        yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
-
-
-    def tearDown(self):
-        """ Empty the proxy db between tests """
-        return calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
-
-
-    def _getPrincipalByShortName(self, type, name):
-        provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
-        return provisioningResource.principalForShortName(type, name)
-
-
-    def _updateMethod(self):
-        """
-        Update a counter in the following test
-        """
-        self.count += 1
-
-
-    def test_expandedMembers(self):
-        """
-        Make sure expandedMembers( ) returns a complete, flattened set of
-        members of a group, including all sub-groups.
-        """
-        bothCoasts = self.directoryService.recordWithShortName(
-            DirectoryService.recordType_groups, "both_coasts")
-        self.assertEquals(
-            set([r.guid for r in bothCoasts.expandedMembers()]),
-            set(['8B4288F6-CC82-491D-8EF9-642EF4F3E7D0',
-                 '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                 '5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                 '5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
-                 'left_coast',
-                 'right_coast'])
-        )
-
-
-    @inlineCallbacks
-    def test_groupMembershipCache(self):
-        """
-        Ensure we get back what we put in
-        """
-        cache = GroupMembershipCache("ProxyDB", expireSeconds=10)
-
-        yield cache.setGroupsFor("a", set(["b", "c", "d"])) # a is in b, c, d
-        members = (yield cache.getGroupsFor("a"))
-        self.assertEquals(members, set(["b", "c", "d"]))
-
-        yield cache.setGroupsFor("b", set()) # b not in any groups
-        members = (yield cache.getGroupsFor("b"))
-        self.assertEquals(members, set())
-
-        cache._memcacheProtocol.advanceClock(10)
-
-        members = (yield cache.getGroupsFor("a")) # has expired
-        self.assertEquals(members, set())
-
-
-    @inlineCallbacks
-    def test_groupMembershipCacheUpdater(self):
-        """
-        Let the GroupMembershipCacheUpdater populate the cache, then make
-        sure proxyFor( ) and groupMemberships( ) work from the cache
-        """
-        cache = GroupMembershipCache("ProxyDB", expireSeconds=60)
-        # Having a groupMembershipCache assigned to the directory service is the
-        # trigger to use such a cache:
-        self.directoryService.groupMembershipCache = cache
-
-        updater = GroupMembershipCacheUpdater(
-            calendaruserproxy.ProxyDBService, self.directoryService, 30, 30, 30,
-            cache=cache, useExternalProxies=False)
-
-        # Exercise getGroups()
-        groups, aliases = (yield updater.getGroups())
-        self.assertEquals(
-            groups,
-            {
-                '00599DAF-3E75-42DD-9DB7-52617E79943F':
-                    set(['46D9D716-CBEE-490F-907A-66FA6C3767FF']),
-                '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1':
-                    set(['8B4288F6-CC82-491D-8EF9-642EF4F3E7D0']),
-                'admin':
-                    set(['9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1']),
-                'both_coasts':
-                    set(['left_coast', 'right_coast']),
-                'grunts':
-                    set(['5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                         '5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
-                         '6423F94A-6B76-4A3A-815B-D52CFD77935D']),
-                'left_coast':
-                    set(['5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
-                         '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                         '8B4288F6-CC82-491D-8EF9-642EF4F3E7D0']),
-                'non_calendar_group':
-                    set(['5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                         '8B4288F6-CC82-491D-8EF9-642EF4F3E7D0']),
-                'recursive1_coasts':
-                    set(['6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                         'recursive2_coasts']),
-                'recursive2_coasts':
-                    set(['5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                         'recursive1_coasts']),
-                'right_coast':
-                    set(['5A985493-EE2C-4665-94CF-4DFEA3A89500'])
-            }
-        )
-        self.assertEquals(
-            aliases,
-            {
-                '00599DAF-3E75-42DD-9DB7-52617E79943F':
-                    '00599DAF-3E75-42DD-9DB7-52617E79943F',
-                '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1':
-                    '9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
-                 'admin': 'admin',
-                 'both_coasts': 'both_coasts',
-                 'grunts': 'grunts',
-                 'left_coast': 'left_coast',
-                 'non_calendar_group': 'non_calendar_group',
-                 'recursive1_coasts': 'recursive1_coasts',
-                 'recursive2_coasts': 'recursive2_coasts',
-                 'right_coast': 'right_coast'
-            }
-        )
-
-        # Exercise expandedMembers()
-        self.assertEquals(
-            updater.expandedMembers(groups, "both_coasts"),
-            set(['5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                 '5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1',
-                 '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                 '8B4288F6-CC82-491D-8EF9-642EF4F3E7D0',
-                 'left_coast',
-                 'right_coast']
-            )
-        )
-
-        # Prevent an update by locking the cache
-        acquiredLock = (yield cache.acquireLock())
-        self.assertTrue(acquiredLock)
-        self.assertEquals((False, 0, 0), (yield updater.updateCache()))
-
-        # You can't lock when already locked:
-        acquiredLockAgain = (yield cache.acquireLock())
-        self.assertFalse(acquiredLockAgain)
-
-        # Allow an update by unlocking the cache
-        yield cache.releaseLock()
-
-        self.assertEquals((False, 9, 9), (yield updater.updateCache()))
-
-        # Verify cache is populated:
-        self.assertTrue((yield cache.isPopulated()))
-
-        delegates = (
-
-            # record name
-            # read-write delegators
-            # read-only delegators
-            # groups delegate is in (restricted to only those groups
-            #   participating in delegation)
-
-            ("wsanchez",
-             set(["mercury", "apollo", "orion", "gemini"]),
-             set(["non_calendar_proxy"]),
-             set(['left_coast',
-                  'both_coasts',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                  'gemini#calendar-proxy-write',
-                ]),
-            ),
-            ("cdaboo",
-             set(["apollo", "orion", "non_calendar_proxy"]),
-             set(["non_calendar_proxy"]),
-             set(['both_coasts',
-                  'non_calendar_group',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                ]),
-            ),
-            ("lecroy",
-             set(["apollo", "mercury", "non_calendar_proxy"]),
-             set(),
-             set(['both_coasts',
-                  'left_coast',
-                  'non_calendar_group',
-                ]),
-            ),
-            ("usera",
-             set(),
-             set(),
-             set(),
-            ),
-            ("userb",
-             set(['7423F94A-6B76-4A3A-815B-D52CFD77935D']),
-             set(),
-             set(['7423F94A-6B76-4A3A-815B-D52CFD77935D#calendar-proxy-write']),
-            ),
-            ("userc",
-             set(['7423F94A-6B76-4A3A-815B-D52CFD77935D']),
-             set(),
-             set(['7423F94A-6B76-4A3A-815B-D52CFD77935D#calendar-proxy-write']),
-            ),
-        )
-
-        for name, write, read, groups in delegates:
-            delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, name)
-
-            proxyFor = (yield delegate.proxyFor(True))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                write,
-            )
-            proxyFor = (yield delegate.proxyFor(False))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                read,
-            )
-            groupsIn = (yield delegate.groupMemberships())
-            uids = set()
-            for group in groupsIn:
-                try:
-                    uid = group.uid # a sub-principal
-                except AttributeError:
-                    uid = group.record.guid # a regular group
-                uids.add(uid)
-            self.assertEquals(
-                set(uids),
-                groups,
-            )
-
-        # Verify CalendarUserProxyPrincipalResource.containsPrincipal( ) works
-        delegator = self._getPrincipalByShortName(DirectoryService.recordType_locations, "mercury")
-        proxyPrincipal = delegator.getChild("calendar-proxy-write")
-        for expected, name in [(True, "wsanchez"), (False, "cdaboo")]:
-            delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, name)
-            self.assertEquals(expected, (yield proxyPrincipal.containsPrincipal(delegate)))
-
-        # Verify that principals who were previously members of delegated-to groups but
-        # are no longer members have their proxyFor info cleaned out of the cache:
-        # Remove wsanchez from all groups in the directory, run the updater, then check
-        # that wsanchez is only a proxy for gemini (since that assignment does not involve groups)
-        self.directoryService.xmlFile = dirTest.child("accounts-modified.xml")
-        self.directoryService._alwaysStat = True
-        self.assertEquals((False, 8, 1), (yield updater.updateCache()))
-        delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, "wsanchez")
-        proxyFor = (yield delegate.proxyFor(True))
-        self.assertEquals(
-          set([p.record.guid for p in proxyFor]),
-          set(['gemini'])
-        )
-
-
-    @inlineCallbacks
-    def test_groupMembershipCacheUpdaterExternalProxies(self):
-        """
-        Exercise external proxy assignment support (assignments come from the
-        directory service itself)
-        """
-        cache = GroupMembershipCache("ProxyDB", expireSeconds=60)
-        # Having a groupMembershipCache assigned to the directory service is the
-        # trigger to use such a cache:
-        self.directoryService.groupMembershipCache = cache
-
-        # This time, we're setting some external proxy assignments for the
-        # "transporter" resource...
-        def fakeExternalProxies():
-            return [
-                (
-                    "transporter#calendar-proxy-write",
-                    set(["6423F94A-6B76-4A3A-815B-D52CFD77935D",
-                         "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"])
-                ),
-                (
-                    "transporter#calendar-proxy-read",
-                    set(["5A985493-EE2C-4665-94CF-4DFEA3A89500"])
-                ),
-            ]
-
-        updater = GroupMembershipCacheUpdater(
-            calendaruserproxy.ProxyDBService, self.directoryService, 30, 30, 30,
-            cache=cache, useExternalProxies=True,
-            externalProxiesSource=fakeExternalProxies)
-
-        yield updater.updateCache()
-
-        delegates = (
-
-            # record name
-            # read-write delegators
-            # read-only delegators
-            # groups delegate is in (restricted to only those groups
-            #   participating in delegation)
-
-            ("wsanchez",
-             set(["mercury", "apollo", "orion", "gemini", "transporter"]),
-             set(["non_calendar_proxy"]),
-             set(['left_coast',
-                  'both_coasts',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                  'gemini#calendar-proxy-write',
-                  'transporter#calendar-proxy-write',
-                ]),
-            ),
-            ("cdaboo",
-             set(["apollo", "orion", "non_calendar_proxy"]),
-             set(["non_calendar_proxy", "transporter"]),
-             set(['both_coasts',
-                  'non_calendar_group',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                  'transporter#calendar-proxy-read',
-                ]),
-            ),
-            ("lecroy",
-             set(["apollo", "mercury", "non_calendar_proxy", "transporter"]),
-             set(),
-             set(['both_coasts',
-                  'left_coast',
-                  'non_calendar_group',
-                  'transporter#calendar-proxy-write',
-                ]),
-            ),
-        )
-
-        for name, write, read, groups in delegates:
-            delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, name)
-
-            proxyFor = (yield delegate.proxyFor(True))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                write,
-            )
-            proxyFor = (yield delegate.proxyFor(False))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                read,
-            )
-            groupsIn = (yield delegate.groupMemberships())
-            uids = set()
-            for group in groupsIn:
-                try:
-                    uid = group.uid # a sub-principal
-                except AttributeError:
-                    uid = group.record.guid # a regular group
-                uids.add(uid)
-            self.assertEquals(
-                set(uids),
-                groups,
-            )
-
-        #
-        # Now remove two external assignments, and those should take effect.
-        #
-        def fakeExternalProxiesRemoved():
-            return [
-                (
-                    "transporter#calendar-proxy-write",
-                    set(["8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"])
-                ),
-            ]
-
-        updater = GroupMembershipCacheUpdater(
-            calendaruserproxy.ProxyDBService, self.directoryService, 30, 30, 30,
-            cache=cache, useExternalProxies=True,
-            externalProxiesSource=fakeExternalProxiesRemoved)
-
-        yield updater.updateCache()
-
-        delegates = (
-
-            # record name
-            # read-write delegators
-            # read-only delegators
-            # groups delegate is in (restricted to only those groups
-            #   participating in delegation)
-
-            # Note: "transporter" is now gone for wsanchez and cdaboo
-
-            ("wsanchez",
-             set(["mercury", "apollo", "orion", "gemini"]),
-             set(["non_calendar_proxy"]),
-             set(['left_coast',
-                  'both_coasts',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                  'gemini#calendar-proxy-write',
-                ]),
-            ),
-            ("cdaboo",
-             set(["apollo", "orion", "non_calendar_proxy"]),
-             set(["non_calendar_proxy"]),
-             set(['both_coasts',
-                  'non_calendar_group',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                ]),
-            ),
-            ("lecroy",
-             set(["apollo", "mercury", "non_calendar_proxy", "transporter"]),
-             set(),
-             set(['both_coasts',
-                  'left_coast',
-                  'non_calendar_group',
-                  'transporter#calendar-proxy-write',
-                ]),
-            ),
-        )
-
-        for name, write, read, groups in delegates:
-            delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, name)
-
-            proxyFor = (yield delegate.proxyFor(True))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                write,
-            )
-            proxyFor = (yield delegate.proxyFor(False))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                read,
-            )
-            groupsIn = (yield delegate.groupMemberships())
-            uids = set()
-            for group in groupsIn:
-                try:
-                    uid = group.uid # a sub-principal
-                except AttributeError:
-                    uid = group.record.guid # a regular group
-                uids.add(uid)
-            self.assertEquals(
-                set(uids),
-                groups,
-            )
-
-        #
-        # Now remove all external assignments, and those should take effect.
-        #
-        def fakeExternalProxiesEmpty():
-            return []
-
-        updater = GroupMembershipCacheUpdater(
-            calendaruserproxy.ProxyDBService, self.directoryService, 30, 30, 30,
-            cache=cache, useExternalProxies=True,
-            externalProxiesSource=fakeExternalProxiesEmpty)
-
-        yield updater.updateCache()
-
-        delegates = (
-
-            # record name
-            # read-write delegators
-            # read-only delegators
-            # groups delegate is in (restricted to only those groups
-            #   participating in delegation)
-
-            # Note: "transporter" is now gone for everyone
-
-            ("wsanchez",
-             set(["mercury", "apollo", "orion", "gemini"]),
-             set(["non_calendar_proxy"]),
-             set(['left_coast',
-                  'both_coasts',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                  'gemini#calendar-proxy-write',
-                ]),
-            ),
-            ("cdaboo",
-             set(["apollo", "orion", "non_calendar_proxy"]),
-             set(["non_calendar_proxy"]),
-             set(['both_coasts',
-                  'non_calendar_group',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                ]),
-            ),
-            ("lecroy",
-             set(["apollo", "mercury", "non_calendar_proxy"]),
-             set(),
-             set(['both_coasts',
-                  'left_coast',
-                      'non_calendar_group',
-                ]),
-            ),
-        )
-
-        for name, write, read, groups in delegates:
-            delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, name)
-
-            proxyFor = (yield delegate.proxyFor(True))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                write,
-            )
-            proxyFor = (yield delegate.proxyFor(False))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                read,
-            )
-            groupsIn = (yield delegate.groupMemberships())
-            uids = set()
-            for group in groupsIn:
-                try:
-                    uid = group.uid # a sub-principal
-                except AttributeError:
-                    uid = group.record.guid # a regular group
-                uids.add(uid)
-            self.assertEquals(
-                set(uids),
-                groups,
-            )
-
-        #
-        # Now add back an external assignments, and those should take effect.
-        #
-        def fakeExternalProxiesAdded():
-            return [
-                (
-                    "transporter#calendar-proxy-write",
-                    set(["8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"])
-                ),
-            ]
-
-        updater = GroupMembershipCacheUpdater(
-            calendaruserproxy.ProxyDBService, self.directoryService, 30, 30, 30,
-            cache=cache, useExternalProxies=True,
-            externalProxiesSource=fakeExternalProxiesAdded)
-
-        yield updater.updateCache()
-
-        delegates = (
-
-            # record name
-            # read-write delegators
-            # read-only delegators
-            # groups delegate is in (restricted to only those groups
-            #   participating in delegation)
-
-            ("wsanchez",
-             set(["mercury", "apollo", "orion", "gemini"]),
-             set(["non_calendar_proxy"]),
-             set(['left_coast',
-                  'both_coasts',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                  'gemini#calendar-proxy-write',
-                ]),
-            ),
-            ("cdaboo",
-             set(["apollo", "orion", "non_calendar_proxy"]),
-             set(["non_calendar_proxy"]),
-             set(['both_coasts',
-                  'non_calendar_group',
-                  'recursive1_coasts',
-                  'recursive2_coasts',
-                ]),
-            ),
-            ("lecroy",
-             set(["apollo", "mercury", "non_calendar_proxy", "transporter"]),
-             set(),
-             set(['both_coasts',
-                  'left_coast',
-                  'non_calendar_group',
-                  'transporter#calendar-proxy-write',
-                ]),
-            ),
-        )
-
-        for name, write, read, groups in delegates:
-            delegate = self._getPrincipalByShortName(DirectoryService.recordType_users, name)
-
-            proxyFor = (yield delegate.proxyFor(True))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                write,
-            )
-            proxyFor = (yield delegate.proxyFor(False))
-            self.assertEquals(
-                set([p.record.guid for p in proxyFor]),
-                read,
-            )
-            groupsIn = (yield delegate.groupMemberships())
-            uids = set()
-            for group in groupsIn:
-                try:
-                    uid = group.uid # a sub-principal
-                except AttributeError:
-                    uid = group.record.guid # a regular group
-                uids.add(uid)
-            self.assertEquals(
-                set(uids),
-                groups,
-            )
-
-
-    def test_diffAssignments(self):
-        """
-        Ensure external proxy assignment diffing works
-        """
-
-        self.assertEquals(
-            (
-                # changed
-                [],
-                # removed
-                [],
-            ),
-            diffAssignments(
-                # old
-                [],
-                # new
-                [],
-            )
-        )
-
-        self.assertEquals(
-            (
-                # changed
-                [],
-                # removed
-                [],
-            ),
-            diffAssignments(
-                # old
-                [("B", set(["3"])), ("A", set(["1", "2"])), ],
-                # new
-                [("A", set(["1", "2"])), ("B", set(["3"])), ],
-            )
-        )
-
-        self.assertEquals(
-            (
-                # changed
-                [("A", set(["1", "2"])), ("B", set(["3"])), ],
-                # removed
-                [],
-            ),
-            diffAssignments(
-                # old
-                [],
-                # new
-                [("A", set(["1", "2"])), ("B", set(["3"])), ],
-            )
-        )
-
-        self.assertEquals(
-            (
-                # changed
-                [],
-                # removed
-                ["A", "B"],
-            ),
-            diffAssignments(
-                # old
-                [("A", set(["1", "2"])), ("B", set(["3"])), ],
-                # new
-                [],
-            )
-        )
-
-        self.assertEquals(
-            (
-                # changed
-                [("A", set(["2"])), ("C", set(["4", "5"])), ("D", set(["6"])), ],
-                # removed
-                ["B"],
-            ),
-            diffAssignments(
-                # old
-                [("A", set(["1", "2"])), ("B", set(["3"])), ("C", set(["4"])), ],
-                # new
-                [("D", set(["6"])), ("C", set(["4", "5"])), ("A", set(["2"])), ],
-            )
-        )
-
-
-    @inlineCallbacks
-    def test_groupMembershipCacheSnapshot(self):
-        """
-        The group membership cache creates a snapshot (a pickle file) of
-        the member -> groups dictionary, and can quickly refresh memcached
-        from that snapshot when restarting the server.
-        """
-        cache = GroupMembershipCache("ProxyDB", expireSeconds=60)
-        # Having a groupMembershipCache assigned to the directory service is the
-        # trigger to use such a cache:
-        self.directoryService.groupMembershipCache = cache
-
-        updater = GroupMembershipCacheUpdater(
-            calendaruserproxy.ProxyDBService, self.directoryService, 30, 30, 30,
-            cache=cache)
-
-        dataRoot = FilePath(config.DataRoot)
-        snapshotFile = dataRoot.child("memberships_cache")
-
-        # Snapshot doesn't exist initially
-        self.assertFalse(snapshotFile.exists())
-
-        # Try a fast update (as when the server starts up for the very first
-        # time), but since the snapshot doesn't exist we fault in from the
-        # directory (fast now is False), and snapshot will get created
-
-        # Note that because fast=True and isPopulated() is False, locking is
-        # ignored:
-        yield cache.acquireLock()
-
-        self.assertFalse((yield cache.isPopulated()))
-        fast, numMembers, numChanged = (yield updater.updateCache(fast=True))
-        self.assertEquals(fast, False)
-        self.assertEquals(numMembers, 9)
-        self.assertEquals(numChanged, 9)
-        self.assertTrue(snapshotFile.exists())
-        self.assertTrue((yield cache.isPopulated()))
-
-        yield cache.releaseLock()
-
-        # Try another fast update where the snapshot already exists (as in a
-        # server-restart scenario), which will only read from the snapshot
-        # as indicated by the return value for "fast".  Note that the cache
-        # is already populated so updateCache( ) in fast mode will not do
-        # anything, and numMembers will be 0.
-        fast, numMembers, numChanged = (yield updater.updateCache(fast=True))
-        self.assertEquals(fast, True)
-        self.assertEquals(numMembers, 0)
-
-        # Try an update which faults in from the directory (fast=False)
-        fast, numMembers, numChanged = (yield updater.updateCache(fast=False))
-        self.assertEquals(fast, False)
-        self.assertEquals(numMembers, 9)
-        self.assertEquals(numChanged, 0)
-
-        # Verify the snapshot contains the pickled dictionary we expect
-        expected = {
-            "46D9D716-CBEE-490F-907A-66FA6C3767FF":
-                set([
-                    u"00599DAF-3E75-42DD-9DB7-52617E79943F",
-                ]),
-            "5A985493-EE2C-4665-94CF-4DFEA3A89500":
-                set([
-                    u"non_calendar_group",
-                    u"recursive1_coasts",
-                    u"recursive2_coasts",
-                    u"both_coasts"
-                ]),
-            "6423F94A-6B76-4A3A-815B-D52CFD77935D":
-                set([
-                    u"left_coast",
-                    u"recursive1_coasts",
-                    u"recursive2_coasts",
-                    u"both_coasts"
-                ]),
-            "5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1":
-                set([
-                    u"left_coast",
-                    u"both_coasts"
-                ]),
-            "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0":
-                set([
-                    u"non_calendar_group",
-                    u"left_coast",
-                    u"both_coasts"
-                ]),
-            "left_coast":
-                 set([
-                     u"both_coasts"
-                 ]),
-            "recursive1_coasts":
-                 set([
-                     u"recursive1_coasts",
-                     u"recursive2_coasts"
-                 ]),
-            "recursive2_coasts":
-                set([
-                    u"recursive1_coasts",
-                    u"recursive2_coasts"
-                ]),
-            "right_coast":
-                set([
-                    u"both_coasts"
-                ])
-        }
-        members = pickle.loads(snapshotFile.getContent())
-        self.assertEquals(members, expected)
-
-        # "Corrupt" the snapshot and verify it is regenerated properly
-        snapshotFile.setContent("xyzzy")
-        cache.delete("group-cacher-populated")
-        fast, numMembers, numChanged = (yield updater.updateCache(fast=True))
-        self.assertEquals(fast, False)
-        self.assertEquals(numMembers, 9)
-        self.assertEquals(numChanged, 9)
-        self.assertTrue(snapshotFile.exists())
-        members = pickle.loads(snapshotFile.getContent())
-        self.assertEquals(members, expected)
-
-
-    def test_autoAcceptMembers(self):
-        """
-        autoAcceptMembers( ) returns an empty list if no autoAcceptGroup is
-        assigned, or the expanded membership if assigned.
-        """
-
-        # No auto-accept-group for "orion" in augments.xml
-        orion = self.directoryService.recordWithGUID("orion")
-        self.assertEquals(orion.autoAcceptMembers(), [])
-
-        # "both_coasts" group assigned to "apollo" in augments.xml
-        apollo = self.directoryService.recordWithGUID("apollo")
-        self.assertEquals(
-            set(apollo.autoAcceptMembers()),
-            set([
-                "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0",
-                 "5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1",
-                 "5A985493-EE2C-4665-94CF-4DFEA3A89500",
-                 "6423F94A-6B76-4A3A-815B-D52CFD77935D",
-                 "right_coast",
-                 "left_coast",
-            ])
-        )
-
-
-    # @inlineCallbacks
-    # def testScheduling(self):
-    #     """
-    #     Exercise schedulePolledGroupCachingUpdate
-    #     """
-
-    #     groupCacher = StubGroupCacher()
-
-
-    #     def decorateTransaction(txn):
-    #         txn._groupCacher = groupCacher
-
-    #     store = yield buildStore(self, None)
-    #     store.callWithNewTransactions(decorateTransaction)
-    #     wp = (yield schedulePolledGroupCachingUpdate(store))
-    #     yield wp.whenExecuted()
-    #     self.assertTrue(groupCacher.called)
-
-    # testScheduling.skip = "Fix WorkProposal to track delayed calls and cancel them"
-
-
-
-class StubGroupCacher(object):
-    def __init__(self):
-        self.called = False
-        self.updateSeconds = 99
-
-
-    def updateCache(self):
-        self.called = True
-
-
-
-class RecordsMatchingTokensTests(TestCase):
-
-    @inlineCallbacks
-    def setUp(self):
-        super(RecordsMatchingTokensTests, self).setUp()
-
-        self.directoryFixture.addDirectoryService(XMLDirectoryService(
-            {
-                'xmlFile' : xmlFile,
-                'augmentService' :
-                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-            }
-        ))
-        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
-
-        # Set up a principals hierarchy for each service we're testing with
-        self.principalRootResources = {}
-        name = self.directoryService.__class__.__name__
-        url = "/" + name + "/"
-
-        provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
-
-        self.site.resource.putChild(name, provisioningResource)
-
-        self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
-
-        yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
-
-
-    def tearDown(self):
-        """ Empty the proxy db between tests """
-        return calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
-
-
-    @inlineCallbacks
-    def test_recordsMatchingTokens(self):
-        """
-        Exercise the default recordsMatchingTokens implementation
-        """
-        records = list((yield self.directoryService.recordsMatchingTokens(["Use", "01"])))
-        self.assertNotEquals(len(records), 0)
-        shorts = [record.shortNames[0] for record in records]
-        self.assertTrue("user01" in shorts)
-
-        records = list((yield self.directoryService.recordsMatchingTokens(['"quotey"'],
-            context=self.directoryService.searchContext_attendee)))
-        self.assertEquals(len(records), 1)
-        self.assertEquals(records[0].shortNames[0], "doublequotes")
-
-        records = list((yield self.directoryService.recordsMatchingTokens(["coast"])))
-        self.assertEquals(len(records), 5)
-
-        records = list((yield self.directoryService.recordsMatchingTokens(["poll"],
-            context=self.directoryService.searchContext_location)))
-        self.assertEquals(len(records), 1)
-        self.assertEquals(records[0].shortNames[0], "apollo")
-
-
-    def test_recordTypesForSearchContext(self):
-        self.assertEquals(
-            [self.directoryService.recordType_locations],
-            self.directoryService.recordTypesForSearchContext("location")
-        )
-        self.assertEquals(
-            [self.directoryService.recordType_resources],
-            self.directoryService.recordTypesForSearchContext("resource")
-        )
-        self.assertEquals(
-            [self.directoryService.recordType_users],
-            self.directoryService.recordTypesForSearchContext("user")
-        )
-        self.assertEquals(
-            [self.directoryService.recordType_groups],
-            self.directoryService.recordTypesForSearchContext("group")
-        )
-        self.assertEquals(
-            set([
-                self.directoryService.recordType_resources,
-                self.directoryService.recordType_users,
-                self.directoryService.recordType_groups
-            ]),
-            set(self.directoryService.recordTypesForSearchContext("attendee"))
-        )
-
-
-
-class GUIDTests(TestCase):
-
-    def setUp(self):
-        self.service = DirectoryService()
-        self.service.setRealm("test")
-        self.service.baseGUID = "0E8E6EC2-8E52-4FF3-8F62-6F398B08A498"
-
-
-    def test_normalizeUUID(self):
-
-        # Ensure that record.guid automatically gets normalized to
-        # uppercase+hyphenated form if the value is one that uuid.UUID( )
-        # recognizes.
-
-        data = (
-            (
-                "0543A85A-D446-4CF6-80AE-6579FA60957F",
-                "0543A85A-D446-4CF6-80AE-6579FA60957F"
-            ),
-            (
-                "0543a85a-d446-4cf6-80ae-6579fa60957f",
-                "0543A85A-D446-4CF6-80AE-6579FA60957F"
-            ),
-            (
-                "0543A85AD4464CF680AE-6579FA60957F",
-                "0543A85A-D446-4CF6-80AE-6579FA60957F"
-            ),
-            (
-                "0543a85ad4464cf680ae6579fa60957f",
-                "0543A85A-D446-4CF6-80AE-6579FA60957F"
-            ),
-            (
-                "foo",
-                "foo"
-            ),
-            (
-                None,
-                None
-            ),
-        )
-        for original, expected in data:
-            self.assertEquals(expected, normalizeUUID(original))
-            record = DirectoryRecord(self.service, "users", original,
-                shortNames=("testing",))
-            self.assertEquals(expected, record.guid)
-
-
-
-class DirectoryServiceTests(TestCase):
-    """
-    Test L{DirectoryService} apis.
-    """
-
-    class StubDirectoryService(DirectoryService):
-
-        def __init__(self):
-            self._records = {}
-
-
-        def createRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-            fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-            uid=None, password=None, **kwargs):
-            """
-            Create/persist a directory record based on the given values
-            """
-
-            record = DirectoryRecord(
-                self,
-                recordType,
-                guid=guid,
-                shortNames=shortNames,
-                authIDs=authIDs,
-                fullName=fullName,
-                firstName=firstName,
-                lastName=lastName,
-                emailAddresses=emailAddresses,
-                uid=uid,
-                password=password,
-                **kwargs
-            )
-            self._records.setdefault(recordType, []).append(record)
-
-
-        def recordTypes(self):
-            return self._records.keys()
-
-
-        def listRecords(self, recordType):
-            return self._records[recordType]
-
-
-    def setUp(self):
-        self.service = self.StubDirectoryService()
-        self.service.setRealm("test")
-        self.service.baseGUID = "0E8E6EC2-8E52-4FF3-8F62-6F398B08A498"
-
-
-    def test_recordWithCalendarUserAddress_principal_uris(self):
-        """
-        Make sure that recordWithCalendarUserAddress handles percent-encoded
-        principal URIs.
-        """
-
-        self.service.createRecord(
-            DirectoryService.recordType_users,
-            guid="user01",
-            shortNames=("user 01", "User 01"),
-            fullName="User 01",
-            enabledForCalendaring=True,
-        )
-        self.service.createRecord(
-            DirectoryService.recordType_users,
-            guid="user02",
-            shortNames=("user02", "User 02"),
-            fullName="User 02",
-            enabledForCalendaring=True,
-        )
-
-        record = self.service.recordWithCalendarUserAddress("/principals/users/user%2001")
-        self.assertTrue(record is not None)
-        record = self.service.recordWithCalendarUserAddress("/principals/users/user02")
-        self.assertTrue(record is not None)
-        record = self.service.recordWithCalendarUserAddress("/principals/users/user%0202")
-        self.assertTrue(record is None)
-
-
-
-class DirectoryRecordTests(TestCase):
-    """
-    Test L{DirectoryRecord} apis.
-    """
-
-    def setUp(self):
-        self.service = DirectoryService()
-        self.service.setRealm("test")
-        self.service.baseGUID = "0E8E6EC2-8E52-4FF3-8F62-6F398B08A498"
-
-
-    def test_cacheToken(self):
-        """
-        Test that DirectoryRecord.cacheToken is different for different records, and its value changes
-        as attributes on the record change.
-        """
-
-        record1 = DirectoryRecord(self.service, "users", str(uuid.uuid4()), shortNames=("testing1",))
-        record2 = DirectoryRecord(self.service, "users", str(uuid.uuid4()), shortNames=("testing2",))
-        self.assertNotEquals(record1.cacheToken(), record2.cacheToken())
-
-        cache1 = record1.cacheToken()
-        record1.enabled = True
-        self.assertNotEquals(cache1, record1.cacheToken())
-
-        cache1 = record1.cacheToken()
-        record1.enabledForCalendaring = True
-        self.assertNotEquals(cache1, record1.cacheToken())

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_guidchange.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_guidchange.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_guidchange.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,116 +0,0 @@
-##
-# Copyright (c) 2005-2014 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 print_function
-
-from twistedcaldav.directory.directory import DirectoryService
-
-from txdav.xml import element as davxml
-from txweb2.dav.resource import AccessDeniedError
-from txweb2.test.test_server import SimpleRequest
-
-from twistedcaldav.directory.test.util import maybeCommit
-from twistedcaldav.test.util import TestCase, xmlFile
-
-
-class ProvisionedPrincipals(TestCase):
-    """
-    Directory service provisioned principals.
-    """
-    def setUp(self):
-        super(ProvisionedPrincipals, self).setUp()
-
-        # Setup the initial directory
-        self.createStockDirectoryService()
-        self.setupCalendars()
-
-        self.site.resource.setAccessControlList(davxml.ACL())
-
-
-    def resetCalendars(self):
-        del self.site.resource.putChildren["calendars"]
-        self.setupCalendars()
-
-
-    def test_guidchange(self):
-        """
-        DirectoryPrincipalResource.proxies()
-        """
-        oldUID = "5A985493-EE2C-4665-94CF-4DFEA3A89500"
-        newUID = "38D8AC00-5490-4425-BE3A-05FFB9862444"
-
-        homeResource = "/calendars/users/cdaboo/"
-
-        def privs1(result):
-            # Change GUID in record
-            self.xmlFile.setContent(
-                xmlFile.getContent().replace(oldUID, newUID)
-            )
-
-            # Force re-read of records (not sure why _fileInfo has to be wiped here...)
-            self.directoryService._fileInfo = (0, 0)
-            self.directoryService.recordWithShortName(DirectoryService.recordType_users, "cdaboo")
-
-            # Now force the calendar home resource to be reset
-            self.resetCalendars()
-
-            # Make sure new user cannot access old user's calendar home
-            return self._checkPrivileges(None, homeResource, davxml.HRef("/principals/__uids__/" + newUID + "/"), davxml.Write, False)
-
-        # Make sure current user has access to their calendar home
-        d = self._checkPrivileges(None, homeResource, davxml.HRef("/principals/__uids__/" + oldUID + "/"), davxml.Write, True)
-        d.addCallback(privs1)
-        return d
-
-    #
-    # This test fails because /calendars/users/cdaboo/ actually is a
-    # different resource (ie. the /calendars/__uids__/... URL would be
-    # different) when the GUID for cdaboo changes.
-    #
-    # The test needs to create a fixed resource with access granted to
-    # the old cdaboo; calendar homes no longer do this.
-    #
-    # Using the __uids__ URL won't work either because the old URL
-    # goes away with the old account.
-    #
-    test_guidchange.todo = "Test no longer works."
-
-    def _checkPrivileges(self, resource, url, principal, privilege, allowed):
-        request = SimpleRequest(self.site, "GET", "/")
-
-        def gotResource(resource):
-            d = resource.checkPrivileges(request, (privilege,), principal=davxml.Principal(principal))
-            if allowed:
-                def onError(f):
-                    f.trap(AccessDeniedError)
-                    #print(resource.readDeadProperty(davxml.ACL).toxml())
-                    self.fail("%s should have %s privilege on %r" % (principal, privilege.sname(), resource))
-                d.addErrback(onError)
-            else:
-                def onError(f):
-                    f.trap(AccessDeniedError)
-                def onSuccess(_):
-                    #print(resource.readDeadProperty(davxml.ACL).toxml())
-                    self.fail("%s should not have %s privilege on %r" % (principal, privilege.sname(), resource))
-                d.addCallback(onSuccess)
-                d.addErrback(onError)
-            def _commit(ignore):
-                maybeCommit(request)
-            d.addBoth(_commit)
-            return d
-
-        d = request.locateResource(url)
-        d.addCallback(gotResource)
-        return d

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_ldapdirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_ldapdirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_ldapdirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,1856 +0,0 @@
-##
-# Copyright (c) 2011-2014 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 print_function
-
-try:
-    from twistedcaldav.directory.ldapdirectory import (
-        buildFilter, buildFilterFromTokens, LdapDirectoryService,
-        MissingGuidException, MissingRecordNameException,
-        normalizeDNstr, dnContainedIn
-    )
-    from twistedcaldav.directory.util import splitIntoBatches
-    from twistedcaldav.test.util import proxiesFile
-    from twistedcaldav.directory.calendaruserproxyloader import (
-        XMLCalendarUserProxyLoader
-    )
-    from twistedcaldav.directory import calendaruserproxy
-    from twistedcaldav.directory.directory import (
-        GroupMembershipCache, GroupMembershipCacheUpdater
-    )
-    from twisted.internet.defer import inlineCallbacks
-    from string import maketrans
-    import ldap
-except ImportError:
-    print("Skipping because ldap module not installed")
-else:
-    from twistedcaldav.test.util import TestCase
-
-    class BuildFilterTestCase(TestCase):
-
-        def test_buildFilter(self):
-            mapping = {
-                "recordName": "uid",
-                "fullName": "cn",
-                "emailAddresses": "mail",
-                "firstName": "givenName",
-                "lastName": "sn",
-                "guid": "generateduid",
-                "memberIDAttr": "generateduid",
-            }
-
-            entries = [
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"starts-with"),
-                        ("emailAddresses", "mor", True, u"starts-with"),
-                        ("firstName", "mor", True, u"starts-with"),
-                        ("lastName", "mor", True, u"starts-with")
-                    ],
-                    "operand": "or",
-                    "recordType": "users",
-                    "expected": "(&(uid=*)(generateduid=*)(|(cn=mor*)(mail=mor*)(givenName=mor*)(sn=mor*)))",
-                    "optimize": False,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor(", True, u"starts-with"),
-                        ("emailAddresses", "mor)", True, u"contains"),
-                        ("firstName", "mor*", True, u"exact"),
-                        ("lastName", "mor\\", True, u"starts-with")
-                    ],
-                    "operand": "or",
-                    "recordType": "users",
-                    "expected": "(&(uid=*)(generateduid=*)(|(cn=mor\\28*)(mail=*mor\\29*)(givenName=mor\\2a)(sn=mor\\5c*)))",
-                    "optimize": False,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"starts-with"),
-                    ],
-                    "operand": "or",
-                    "recordType": "users",
-                    "expected": "(&(uid=*)(generateduid=*)(cn=mor*))",
-                    "optimize": False,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"contains"),
-                        ("emailAddresses", "mor", True, u"equals"),
-                        ("invalid", "mor", True, u"starts-with"),
-                    ],
-                    "operand": "and",
-                    "recordType": "users",
-                    "expected": "(&(uid=*)(generateduid=*)(&(cn=*mor*)(mail=mor)))",
-                    "optimize": False,
-                },
-                {
-                    "fields": [
-                        ("invalid", "mor", True, u"contains"),
-                        ("invalid", "mor", True, u"starts-with"),
-                    ],
-                    "operand": "and",
-                    "recordType": "users",
-                    "expected": None,
-                    "optimize": False,
-                },
-                {
-                    "fields": [],
-                    "operand": "and",
-                    "recordType": "users",
-                    "expected": None,
-                    "optimize": False,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"starts-with"),
-                        ("fullName", "sag", True, u"starts-with"),
-                        ("emailAddresses", "mor", True, u"starts-with"),
-                        ("emailAddresses", "sag", True, u"starts-with"),
-                        ("firstName", "mor", True, u"starts-with"),
-                        ("firstName", "sag", True, u"starts-with"),
-                        ("lastName", "mor", True, u"starts-with"),
-                        ("lastName", "sag", True, u"starts-with"),
-                    ],
-                    "operand": "or",
-                    "recordType": "users",
-                    "expected": "(&(uid=*)(generateduid=*)(|(&(givenName=mor*)(sn=sag*))(&(givenName=sag*)(sn=mor*))))",
-                    "optimize": True,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"starts-with"),
-                        ("fullName", "sag", True, u"starts-with"),
-                        ("emailAddresses", "mor", True, u"starts-with"),
-                        ("emailAddresses", "sag", True, u"starts-with"),
-                        ("firstName", "mor", True, u"starts-with"),
-                        ("firstName", "sag", True, u"starts-with"),
-                        ("lastName", "mor", True, u"starts-with"),
-                        ("lastName", "sag", True, u"starts-with"),
-                    ],
-                    "operand": "or",
-                    "recordType": "groups",
-                    "expected": None,
-                    "optimize": True,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"starts-with"),
-                        ("fullName", "sag", True, u"starts-with"),
-                        ("emailAddresses", "mor", True, u"starts-with"),
-                        ("emailAddresses", "sag", True, u"starts-with"),
-                        ("firstName", "mor", True, u"starts-with"),
-                        ("firstName", "sag", True, u"starts-with"),
-                        ("lastName", "mor", True, u"starts-with"),
-                        ("lastName", "sag", True, u"starts-with"),
-                    ],
-                    "operand": "or",
-                    "recordType": "groups",
-                    "expected": None,
-                    "optimize": True,
-                },
-                {
-                    "fields": [
-                        ("guid", "xyzzy", True, u"equals"),
-                        ("guid", "plugh", True, u"equals"),
-                    ],
-                    "operand": "or",
-                    "recordType": "groups",
-                    "expected": "(&(uid=*)(generateduid=*)(|(generateduid=xyzzy)(generateduid=plugh)))",
-                    "optimize": True,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"contains"),
-                        ("fullName", "sag", True, u"contains"),
-                    ],
-                    "operand": "or",
-                    "recordType": "locations",
-                    "expected": "(&(uid=*)(generateduid=*)(|(cn=*mor*)(cn=*sag*)))",
-                    "optimize": True,
-                },
-                {
-                    "fields": [
-                        ("fullName", "mor", True, u"contains"),
-                        ("fullName", "sag", True, u"contains"),
-                    ],
-                    "operand": "or",
-                    "recordType": "resources",
-                    "expected": "(&(uid=*)(generateduid=*)(|(cn=*mor*)(cn=*sag*)))",
-                    "optimize": True,
-                },
-            ]
-            for entry in entries:
-                self.assertEquals(
-                    buildFilter(entry["recordType"], mapping, entry["fields"],
-                                operand=entry["operand"], optimizeMultiName=entry["optimize"]),
-                    entry["expected"]
-                )
-
-
-    class BuildFilterFromTokensTestCase(TestCase):
-
-        def test_buildFilterFromTokens(self):
-
-            entries = [
-                {
-                    "tokens": ["foo"],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": "mail",
-                    },
-                    "expected": "(&(a=b)(|(cn=*foo*)(mail=foo*)))",
-                    "extra": "(a=b)",
-                },
-                {
-                    "tokens": ["foo", "foo", "oo", "fo", "bar"],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": "mail",
-                    },
-                    "expected": "(&(a=b)(|(cn=*bar*)(mail=bar*))(|(cn=*foo*)(mail=foo*)))",
-                    "extra": "(a=b)",
-                },
-                {
-                    "tokens": ["fo", "foo", "foooo", "ooo", "fooo"],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": "mail",
-                    },
-                    "expected": "(&(a=b)(|(cn=*foooo*)(mail=foooo*)))",
-                    "extra": "(a=b)",
-                },
-                {
-                    "tokens": ["foo"],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": ["mail", "mailAliases"],
-                    },
-                    "expected": "(&(a=b)(|(cn=*foo*)(mail=foo*)(mailAliases=foo*)))",
-                    "extra": "(a=b)",
-                },
-                {
-                    "tokens": [],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": "mail",
-                    },
-                    "expected": None,
-                    "extra": None,
-                },
-                {
-                    "tokens": ["foo", "bar"],
-                    "mapping": {},
-                    "expected": None,
-                    "extra": None,
-                },
-                {
-                    "tokens": ["foo", "bar"],
-                    "mapping": {
-                        "emailAddresses": "mail",
-                    },
-                    "expected": "(&(mail=bar*)(mail=foo*))",
-                    "extra": None,
-                },
-                {
-                    "tokens": ["foo", "bar"],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": "mail",
-                    },
-                    "expected": "(&(|(cn=*bar*)(mail=bar*))(|(cn=*foo*)(mail=foo*)))",
-                    "extra": None,
-                },
-                {
-                    "tokens": ["foo", "bar"],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": ["mail", "mailAliases"],
-                    },
-                    "expected": "(&(|(cn=*bar*)(mail=bar*)(mailAliases=bar*))(|(cn=*foo*)(mail=foo*)(mailAliases=foo*)))",
-                    "extra": None,
-                },
-                {
-                    "tokens": ["foo", "bar", "baz("],
-                    "mapping": {
-                        "fullName": "cn",
-                        "emailAddresses": "mail",
-                    },
-                    "expected": "(&(|(cn=*bar*)(mail=bar*))(|(cn=*baz\\28*)(mail=baz\\28*))(|(cn=*foo*)(mail=foo*)))",
-                    "extra": None,
-                },
-            ]
-            for entry in entries:
-                self.assertEquals(
-                    buildFilterFromTokens(None, entry["mapping"], entry["tokens"], extra=entry["extra"]),
-                    entry["expected"]
-                )
-
-
-    class StubList(object):
-        def __init__(self, wrapper):
-            self.ldap = wrapper
-
-        def startSearch(self, base, scope, filterstr, attrList=None,
-                        timeout=-1, sizelimit=0):
-            self.base = base
-            self.scope = scope
-            self.filterstr = filterstr
-            self.attrList = attrList
-            self.timeout = timeout
-            self.sizelimit = sizelimit
-
-        def processResults(self):
-            self.allResults = self.ldap.search_s(self.base, self.scope,
-                                                 self.filterstr,
-                                                 attrlist=self.attrList)
-
-
-    class StubAsync(object):
-        def List(self, wrapper):
-            return StubList(wrapper)
-
-
-    class LdapDirectoryTestWrapper(object):
-        """
-        A test stub which replaces search_s( ) with a version that will return
-        whatever you have previously called addTestResults( ) with.
-        """
-
-        def __init__(self, actual, records):
-            self.actual = actual
-            self.async = StubAsync()
-
-            # Test data returned from search_s.
-            # Note that some DNs have various extra whitespace added and mixed
-            # up case since LDAP is pretty loose about these.
-            self.records = records
-
-
-        def search_s(self, base, scope, filterstr="(objectClass=*)",
-                     attrlist=None):
-            """ A simple implementation of LDAP search filter processing """
-
-            base = normalizeDNstr(base)
-            results = []
-            for dn, attrs in self.records:
-                dn = normalizeDNstr(dn)
-                if dn == base:
-                    results.append(("ignored", (dn, attrs)))
-                elif dnContainedIn(ldap.dn.str2dn(dn), ldap.dn.str2dn(base)):
-                    if filterstr in ("(objectClass=*)", "(!(objectClass=organizationalUnit))"):
-                        results.append(("ignored", (dn, attrs)))
-                    else:
-                        trans = maketrans("&(|)", "   |")
-                        fragments = filterstr.encode("utf-8").translate(trans).split("|")
-                        for fragment in fragments:
-                            if not fragment:
-                                continue
-                            fragment = fragment.strip()
-                            key, value = fragment.split("=")
-                            if value in attrs.get(key, []):
-                                results.append(("ignored", (dn, attrs)))
-                                break
-                            elif value == "*" and key in attrs:
-                                results.append(("ignored", (dn, attrs)))
-                                break
-
-            return results
-
-
-    class LdapDirectoryServiceTestCase(TestCase):
-
-        nestedUsingDifferentAttributeUsingDN = (
-            (
-                (
-                    "cn=Recursive1_coasts, cn=gROUps,dc=example, dc=com",
-                    {
-                        'cn': ['recursive1_coasts'],
-                        'apple-generateduid': ['recursive1_coasts'],
-                        'uniqueMember': [
-                            'uid=wsanchez ,cn=users, dc=eXAMple,dc=com',
-                        ],
-                        'nestedGroups': [
-                            'cn=recursive2_coasts,cn=groups,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    "cn=recursive2_coasts,cn=groups,dc=example,dc=com",
-                    {
-                        'cn': ['recursive2_coasts'],
-                        'apple-generateduid': ['recursive2_coasts'],
-                        'uniqueMember': [
-                            'uid=cdaboo,cn=users,dc=example,dc=com',
-                        ],
-                        'nestedGroups': [
-                            'cn=recursive1_coasts,cn=groups,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    'cn=both_coasts,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['both_coasts'],
-                        'apple-generateduid': ['both_coasts'],
-                        'nestedGroups': [
-                            'cn=right_coast,cn=groups,dc=example,dc=com',
-                            'cn=left_coast,cn=groups,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    'cn=right_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['right_coast'],
-                        'apple-generateduid': ['right_coast'],
-                        'uniqueMember': [
-                            'uid=cdaboo,cn=users,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    'cn=left_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['left_coast'],
-                        'apple-generateduid': ['left_coast'],
-                        'uniqueMember': [
-                            'uid=wsanchez, cn=users,dc=example,dc=com',
-                            'uid=lecroy,cn=users,dc=example,dc=com',
-                            'uid=dreid,cn=users,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    "uid=odtestamanda,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestamanda'],
-                        # purposely throw in an un-normalized GUID
-                        'apple-generateduid': ['9dc04a70-e6dd-11df-9492-0800200c9a66'],
-                        'sn': ['Test'],
-                        'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                        'givenName': ['Amanda'],
-                        'cn': ['Amanda Test']
-                    }
-                ),
-                (
-                    "uid=odtestbetty,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestbetty'],
-                        'apple-generateduid': ['93A8F5C5-49D8-4641-840F-CD1903B0394C'],
-                        'sn': ['Test'],
-                        'mail': ['odtestbetty at example.com'],
-                        'givenName': ['Betty'],
-                        'cn': ['Betty Test']
-                    }
-                ),
-                (
-                    "uid=odtestcarlene,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestcarlene'],
-                        # Note: no guid here, to test this record is skipped
-                        'sn': ['Test'],
-                        'mail': ['odtestcarlene at example.com'],
-                        'givenName': ['Carlene'],
-                        'cn': ['Carlene Test']
-                    }
-                ),
-                (
-                    "uid=cdaboo,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['cdaboo'],
-                        'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                        'sn': ['Daboo'],
-                        'mail': ['daboo at example.com'],
-                        'givenName': ['Cyrus'],
-                        'cn': ['Cyrus Daboo']
-                    }
-                ),
-                (
-                    "uid=wsanchez  ,  cn=users  , dc=example,dc=com",
-                    {
-                        'uid': ['wsanchez'],
-                        'apple-generateduid': ['6423F94A-6B76-4A3A-815B-D52CFD77935D'],
-                        'sn': ['Sanchez'],
-                        'mail': ['wsanchez at example.com'],
-                        'givenName': ['Wilfredo'],
-                        'cn': ['Wilfredo Sanchez']
-                    }
-                ),
-                (
-                    "uid=testresource  ,  cn=resources  , dc=example,dc=com",
-                    {
-                        'uid': ['testresource'],
-                        'apple-generateduid': ['D91B21B9-B856-495A-8E36-0E5AD54EFB3A'],
-                        'sn': ['Resource'],
-                        'givenName': ['Test'],
-                        'cn': ['Test Resource'],
-                        # purposely throw in an un-normalized GUID
-                        'read-write-proxy': ['6423f94a-6b76-4a3a-815b-d52cfd77935d'],
-                        'read-only-proxy': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                    }
-                ),
-                (
-                    "uid=testresource2  ,  cn=resources  , dc=example,dc=com",
-                    {
-                        'uid': ['testresource2'],
-                        'apple-generateduid': ['753E5A60-AFFD-45E4-BF2C-31DAB459353F'],
-                        'sn': ['Resource2'],
-                        'givenName': ['Test'],
-                        'cn': ['Test Resource2'],
-                        'read-write-proxy': ['6423F94A-6B76-4A3A-815B-D52CFD77935D'],
-                    }
-                ),
-            ),
-            {
-                "augmentService": None,
-                "groupMembershipCache": None,
-                "cacheTimeout": 1,  # Minutes
-                "negativeCaching": False,
-                "warningThresholdSeconds": 3,
-                "batchSize": 500,
-                "queryLocationsImplicitly": True,
-                "restrictEnabledRecords": True,
-                "restrictToGroup": "both_coasts",
-                "recordTypes": ("users", "groups", "locations", "resources"),
-                "uri": "ldap://localhost/",
-                "tls": False,
-                "tlsCACertFile": None,
-                "tlsCACertDir": None,
-                "tlsRequireCert": None,  # never, allow, try, demand, hard
-                "credentials": {
-                    "dn": None,
-                    "password": None,
-                },
-                "authMethod": "LDAP",
-                "rdnSchema": {
-                    "base": "dc=example,dc=com",
-                    "guidAttr": "apple-generateduid",
-                    "users": {
-                        "rdn": "cn=Users",
-                        "attr": "uid",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "loginEnabledAttr": "",  # attribute controlling login
-                        "loginEnabledValue": "yes",  # "True" value of above attribute
-                        "calendarEnabledAttr": "enable-calendar",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "uid",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "groups": {
-                        "rdn": "cn=Groups",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                        },
-                    },
-                    "locations": {
-                        "rdn": "cn=Places",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "associatedAddressAttr": "assocAddr",
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": "",  # old style, single string
-                        },
-                    },
-                    "resources": {
-                        "rdn": "cn=Resources",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": [],  # new style, array
-                        },
-                    },
-                    "addresses": {
-                        "rdn": "cn=Buildings",
-                        "geoAttr": "coordinates",
-                        "streetAddressAttr": "postal",
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                        },
-                    },
-                },
-                "groupSchema": {
-                    "membersAttr": "uniqueMember",  # how members are specified
-                    "nestedGroupsAttr": "nestedGroups",  # how nested groups are specified
-                    "memberIdAttr": "",  # which attribute the above refer to
-                },
-                "resourceSchema": {
-                    "resourceInfoAttr": "apple-resource-info",  # contains location/resource info
-                    "autoScheduleAttr": None,
-                    "proxyAttr": "read-write-proxy",
-                    "readOnlyProxyAttr": "read-only-proxy",
-                    "autoAcceptGroupAttr": None,
-                },
-                "poddingSchema": {
-                    "serverIdAttr": "server-id",  # maps to augments server-id
-                },
-            }
-        )
-        nestedUsingSameAttributeUsingDN = (
-            (
-                (
-                    "cn=Recursive1_coasts, cn=gROUps,dc=example, dc=com",
-                    {
-                        'cn': ['recursive1_coasts'],
-                        'apple-generateduid': ['recursive1_coasts'],
-                        'uniqueMember': [
-                            'uid=wsanchez ,cn=users, dc=eXAMple,dc=com',
-                            'cn=recursive2_coasts,cn=groups,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    "cn=recursive2_coasts,cn=groups,dc=example,dc=com",
-                    {
-                        'cn': ['recursive2_coasts'],
-                        'apple-generateduid': ['recursive2_coasts'],
-                        'uniqueMember': [
-                            'uid=cdaboo,cn=users,dc=example,dc=com',
-                            'cn=recursive1_coasts,cn=groups,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    'cn=both_coasts,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['both_coasts'],
-                        'apple-generateduid': ['both_coasts'],
-                        'uniqueMember': [
-                            'cn=right_coast,cn=groups,dc=example,dc=com',
-                            'cn=left_coast,cn=groups,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    'cn=right_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['right_coast'],
-                        'apple-generateduid': ['right_coast'],
-                        'uniqueMember': [
-                            'uid=cdaboo,cn=users,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    'cn=left_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['left_coast'],
-                        'apple-generateduid': ['left_coast'],
-                        'uniqueMember': [
-                            'uid=wsanchez, cn=users,dc=example,dc=com',
-                            'uid=lecroy,cn=users,dc=example,dc=com',
-                            'uid=dreid,cn=users,dc=example,dc=com',
-                        ],
-                    }
-                ),
-                (
-                    "uid=odtestamanda,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestamanda'],
-                        'apple-generateduid': ['9DC04A70-E6DD-11DF-9492-0800200C9A66'],
-                        'sn': ['Test'],
-                        'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                        'givenName': ['Amanda'],
-                        'cn': ['Amanda Test']
-                    }
-                ),
-                (
-                    "uid=odtestbetty,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestbetty'],
-                        'apple-generateduid': ['93A8F5C5-49D8-4641-840F-CD1903B0394C'],
-                        'sn': ['Test'],
-                        'mail': ['odtestbetty at example.com'],
-                        'givenName': ['Betty'],
-                        'cn': ['Betty Test']
-                    }
-                ),
-                (
-                    "uid=odtestcarlene,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestcarlene'],
-                        # Note: no guid here, to test this record is skipped
-                        'sn': ['Test'],
-                        'mail': ['odtestcarlene at example.com'],
-                        'givenName': ['Carlene'],
-                        'cn': ['Carlene Test']
-                    }
-                ),
-                (
-                    "uid=cdaboo,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['cdaboo'],
-                        'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                        'sn': ['Daboo'],
-                        'mail': ['daboo at example.com'],
-                        'givenName': ['Cyrus'],
-                        'cn': ['Cyrus Daboo']
-                    }
-                ),
-                (
-                    "uid=wsanchez  ,  cn=users  , dc=example,dc=com",
-                    {
-                        'uid': ['wsanchez'],
-                        'apple-generateduid': ['6423F94A-6B76-4A3A-815B-D52CFD77935D'],
-                        'sn': ['Sanchez'],
-                        'mail': ['wsanchez at example.com'],
-                        'givenName': ['Wilfredo'],
-                        'cn': ['Wilfredo Sanchez']
-                    }
-                ),
-            ),
-            {
-                "augmentService": None,
-                "groupMembershipCache": None,
-                "cacheTimeout": 1,  # Minutes
-                "negativeCaching": False,
-                "warningThresholdSeconds": 3,
-                "batchSize": 500,
-                "queryLocationsImplicitly": True,
-                "restrictEnabledRecords": True,
-                "restrictToGroup": "both_coasts",
-                "recordTypes": ("users", "groups", "locations", "resources"),
-                "uri": "ldap://localhost/",
-                "tls": False,
-                "tlsCACertFile": None,
-                "tlsCACertDir": None,
-                "tlsRequireCert": None,  # never, allow, try, demand, hard
-                "credentials": {
-                    "dn": None,
-                    "password": None,
-                },
-                "authMethod": "LDAP",
-                "rdnSchema": {
-                    "base": "dc=example,dc=com",
-                    "guidAttr": "apple-generateduid",
-                    "users": {
-                        "rdn": "cn=Users",
-                        "attr": "uid",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "loginEnabledAttr": "",  # attribute controlling login
-                        "loginEnabledValue": "yes",  # "True" value of above attribute
-                        "calendarEnabledAttr": "enable-calendar",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "uid",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "groups": {
-                        "rdn": "cn=Groups",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "locations": {
-                        "rdn": "cn=Places",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": "",  # old style, single string
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "resources": {
-                        "rdn": "cn=Resources",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": [],  # new style, array
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                },
-                "groupSchema": {
-                    "membersAttr": "uniqueMember",  # how members are specified
-                    "nestedGroupsAttr": "",  # how nested groups are specified
-                    "memberIdAttr": "",  # which attribute the above refer to
-                },
-                "resourceSchema": {
-                    "resourceInfoAttr": "apple-resource-info",  # contains location/resource info
-                    "autoScheduleAttr": None,
-                    "proxyAttr": None,
-                    "readOnlyProxyAttr": None,
-                    "autoAcceptGroupAttr": None,
-                },
-                "poddingSchema": {
-                    "serverIdAttr": "server-id",  # maps to augments server-id
-                },
-            }
-        )
-        nestedUsingDifferentAttributeUsingGUID = (
-            (
-                (
-                    "cn=Recursive1_coasts, cn=gROUps,dc=example, dc=com",
-                    {
-                        'cn': ['recursive1_coasts'],
-                        'apple-generateduid': ['recursive1_coasts'],
-                        'uniqueMember': [
-                            '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                        ],
-                        'nestedGroups': [
-                            'recursive2_coasts',
-                        ],
-                    }
-                ),
-                (
-                    "cn=recursive2_coasts,cn=groups,dc=example,dc=com",
-                    {
-                        'cn': ['recursive2_coasts'],
-                        'apple-generateduid': ['recursive2_coasts'],
-                        'uniqueMember': [
-                            '5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                        ],
-                        'nestedGroups': [
-                            'recursive1_coasts',
-                        ],
-                    }
-                ),
-                (
-                    'cn=both_coasts,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['both_coasts'],
-                        'apple-generateduid': ['both_coasts'],
-                        'nestedGroups': [
-                            'right_coast',
-                            'left_coast',
-                        ],
-                    }
-                ),
-                (
-                    'cn=right_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['right_coast'],
-                        'apple-generateduid': ['right_coast'],
-                        'uniqueMember': [
-                            '5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                        ],
-                    }
-                ),
-                (
-                    'cn=left_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['left_coast'],
-                        'apple-generateduid': ['left_coast'],
-                        'uniqueMember': [
-                            '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                        ],
-                    }
-                ),
-                (
-                    "uid=odtestamanda,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestamanda'],
-                        'apple-generateduid': ['9DC04A70-E6DD-11DF-9492-0800200C9A66'],
-                        'sn': ['Test'],
-                        'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                        'givenName': ['Amanda'],
-                        'cn': ['Amanda Test']
-                    }
-                ),
-                (
-                    "uid=odtestbetty,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestbetty'],
-                        'apple-generateduid': ['93A8F5C5-49D8-4641-840F-CD1903B0394C'],
-                        'sn': ['Test'],
-                        'mail': ['odtestbetty at example.com'],
-                        'givenName': ['Betty'],
-                        'cn': ['Betty Test']
-                    }
-                ),
-                (
-                    "uid=odtestcarlene,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestcarlene'],
-                        # Note: no guid here, to test this record is skipped
-                        'sn': ['Test'],
-                        'mail': ['odtestcarlene at example.com'],
-                        'givenName': ['Carlene'],
-                        'cn': ['Carlene Test']
-                    }
-                ),
-                (
-                    "uid=cdaboo,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['cdaboo'],
-                        'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                        'sn': ['Daboo'],
-                        'mail': ['daboo at example.com'],
-                        'givenName': ['Cyrus'],
-                        'cn': ['Cyrus Daboo']
-                    }
-                ),
-                (
-                    "uid=wsanchez  ,  cn=users  , dc=example,dc=com",
-                    {
-                        'uid': ['wsanchez'],
-                        'apple-generateduid': ['6423F94A-6B76-4A3A-815B-D52CFD77935D'],
-                        'sn': ['Sanchez'],
-                        'mail': ['wsanchez at example.com'],
-                        'givenName': ['Wilfredo'],
-                        'cn': ['Wilfredo Sanchez']
-                    }
-                ),
-            ),
-            {
-                "augmentService": None,
-                "groupMembershipCache": None,
-                "cacheTimeout": 1,  # Minutes
-                "negativeCaching": False,
-                "warningThresholdSeconds": 3,
-                "batchSize": 500,
-                "queryLocationsImplicitly": True,
-                "restrictEnabledRecords": True,
-                "restrictToGroup": "both_coasts",
-                "recordTypes": ("users", "groups", "locations", "resources"),
-                "uri": "ldap://localhost/",
-                "tls": False,
-                "tlsCACertFile": None,
-                "tlsCACertDir": None,
-                "tlsRequireCert": None,  # never, allow, try, demand, hard
-                "credentials": {
-                    "dn": None,
-                    "password": None,
-                },
-                "authMethod": "LDAP",
-                "rdnSchema": {
-                    "base": "dc=example,dc=com",
-                    "guidAttr": "apple-generateduid",
-                    "users": {
-                        "rdn": "cn=Users",
-                        "attr": "uid",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "loginEnabledAttr": "",  # attribute controlling login
-                        "loginEnabledValue": "yes",  # "True" value of above attribute
-                        "calendarEnabledAttr": "enable-calendar",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "uid",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "groups": {
-                        "rdn": "cn=Groups",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "locations": {
-                        "rdn": "cn=Places",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": "",  # old style, single string
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "resources": {
-                        "rdn": "cn=Resources",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": [],  # new style, array
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                },
-                "groupSchema": {
-                    "membersAttr": "uniqueMember",  # how members are specified
-                    "nestedGroupsAttr": "nestedGroups",  # how nested groups are specified
-                    "memberIdAttr": "apple-generateduid",  # which attribute the above refer to
-                },
-                "resourceSchema": {
-                    "resourceInfoAttr": "apple-resource-info",  # contains location/resource info
-                    "autoScheduleAttr": None,
-                    "proxyAttr": None,
-                    "readOnlyProxyAttr": None,
-                    "autoAcceptGroupAttr": None,
-                },
-                "poddingSchema": {
-                    "serverIdAttr": "server-id",  # maps to augments server-id
-                },
-            }
-        )
-        nestedUsingSameAttributeUsingGUID = (
-            (
-                (
-                    "cn=Recursive1_coasts, cn=gROUps,dc=example, dc=com",
-                    {
-                        'cn': ['recursive1_coasts'],
-                        'apple-generateduid': ['recursive1_coasts'],
-                        'uniqueMember': [
-                            '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                            'recursive2_coasts',
-                        ],
-                    }
-                ),
-                (
-                    "cn=recursive2_coasts,cn=groups,dc=example,dc=com",
-                    {
-                        'cn': ['recursive2_coasts'],
-                        'apple-generateduid': ['recursive2_coasts'],
-                        'uniqueMember': [
-                            '5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                            'recursive1_coasts',
-                        ],
-                    }
-                ),
-                (
-                    'cn=both_coasts,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['both_coasts'],
-                        'apple-generateduid': ['both_coasts'],
-                        'uniqueMember': [
-                            'right_coast',
-                            'left_coast',
-                        ],
-                    }
-                ),
-                (
-                    'cn=right_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['right_coast'],
-                        'apple-generateduid': ['right_coast'],
-                        'uniqueMember': [
-                            '5A985493-EE2C-4665-94CF-4DFEA3A89500',
-                        ],
-                    }
-                ),
-                (
-                    'cn=left_coast,cn=groups,dc=example,dc=com',
-                    {
-                        'cn': ['left_coast'],
-                        'apple-generateduid': ['left_coast'],
-                        'uniqueMember': [
-                            '6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                        ],
-                    }
-                ),
-                (
-                    "uid=odtestamanda,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestamanda'],
-                        'apple-generateduid': ['9DC04A70-E6DD-11DF-9492-0800200C9A66'],
-                        'sn': ['Test'],
-                        'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                        'givenName': ['Amanda'],
-                        'cn': ['Amanda Test']
-                    }
-                ),
-                (
-                    "uid=odtestbetty,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestbetty'],
-                        'apple-generateduid': ['93A8F5C5-49D8-4641-840F-CD1903B0394C'],
-                        'sn': ['Test'],
-                        'mail': ['odtestbetty at example.com'],
-                        'givenName': ['Betty'],
-                        'cn': ['Betty Test']
-                    }
-                ),
-                (
-                    "uid=odtestcarlene,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['odtestcarlene'],
-                        # Note: no guid here, to test this record is skipped
-                        'sn': ['Test'],
-                        'mail': ['odtestcarlene at example.com'],
-                        'givenName': ['Carlene'],
-                        'cn': ['Carlene Test']
-                    }
-                ),
-                (
-                    "uid=cdaboo,cn=users,dc=example,dc=com",
-                    {
-                        'uid': ['cdaboo'],
-                        'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                        'sn': ['Daboo'],
-                        'mail': ['daboo at example.com'],
-                        'givenName': ['Cyrus'],
-                        'cn': ['Cyrus Daboo']
-                    }
-                ),
-                (
-                    "uid=wsanchez  ,  cn=users  , dc=example,dc=com",
-                    {
-                        'uid': ['wsanchez'],
-                        'apple-generateduid': ['6423F94A-6B76-4A3A-815B-D52CFD77935D'],
-                        'sn': ['Sanchez'],
-                        'mail': ['wsanchez at example.com'],
-                        'givenName': ['Wilfredo'],
-                        'cn': ['Wilfredo Sanchez']
-                    }
-                ),
-            ),
-            {
-                "augmentService": None,
-                "groupMembershipCache": None,
-                "cacheTimeout": 1,  # Minutes
-                "negativeCaching": False,
-                "warningThresholdSeconds": 3,
-                "batchSize": 500,
-                "queryLocationsImplicitly": True,
-                "restrictEnabledRecords": True,
-                "restrictToGroup": "both_coasts",
-                "recordTypes": ("users", "groups", "locations", "resources"),
-                "uri": "ldap://localhost/",
-                "tls": False,
-                "tlsCACertFile": None,
-                "tlsCACertDir": None,
-                "tlsRequireCert": None,  # never, allow, try, demand, hard
-                "credentials": {
-                    "dn": None,
-                    "password": None,
-                },
-                "authMethod": "LDAP",
-                "rdnSchema": {
-                    "base": "dc=example,dc=com",
-                    "guidAttr": "apple-generateduid",
-                    "users": {
-                        "rdn": "cn=Users",
-                        "attr": "uid",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "loginEnabledAttr": "",  # attribute controlling login
-                        "loginEnabledValue": "yes",  # "True" value of above attribute
-                        "calendarEnabledAttr": "enable-calendar",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "uid",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "groups": {
-                        "rdn": "cn=Groups",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "",  # additional filter for this type
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": ["mail", "emailAliases"],
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "locations": {
-                        "rdn": "cn=Places",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": "",  # old style, single string
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                    "resources": {
-                        "rdn": "cn=Resources",
-                        "attr": "cn",  # used only to synthesize email address
-                        "emailSuffix": None,  # used only to synthesize email address
-                        "filter": "(objectClass=apple-resource)",  # additional filter for this type
-                        "calendarEnabledAttr": "",  # attribute controlling calendaring
-                        "calendarEnabledValue": "yes",  # "True" value of above attribute
-                        "mapping": {  # maps internal record names to LDAP
-                            "recordName": "cn",
-                            "fullName": "cn",
-                            "emailAddresses": [],  # new style, array
-                            "firstName": "givenName",
-                            "lastName": "sn",
-                        },
-                    },
-                },
-                "groupSchema": {
-                    "membersAttr": "uniqueMember",  # how members are specified
-                    "nestedGroupsAttr": "",  # how nested groups are specified
-                    "memberIdAttr": "apple-generateduid",  # which attribute the above refer to
-                },
-                "resourceSchema": {
-                    "resourceInfoAttr": "apple-resource-info",  # contains location/resource info
-                    "autoScheduleAttr": None,
-                    "proxyAttr": None,
-                    "readOnlyProxyAttr": None,
-                    "autoAcceptGroupAttr": None,
-                },
-                "poddingSchema": {
-                    "serverIdAttr": "server-id",  # maps to augments server-id
-                },
-            }
-        )
-
-
-        def setupService(self, scenario):
-            self.service = LdapDirectoryService(scenario[1])
-            self.service.ldap = LdapDirectoryTestWrapper(self.service.ldap, scenario[0])
-            self.patch(ldap, "async", StubAsync())
-
-
-        def test_ldapWrapper(self):
-            """
-            Exercise the fake search_s implementation
-            """
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-
-            # Get all groups
-            self.assertEquals(
-                len(self.service.ldap.search_s("cn=groups,dc=example,dc=com", 0, "(objectClass=*)", [])), 5)
-
-            self.assertEquals(
-                len(self.service.ldap.search_s("cn=recursive1_coasts,cn=groups,dc=example,dc=com", 2, "(objectClass=*)", [])), 1)
-
-            self.assertEquals(
-                len(self.service.ldap.search_s("cn=groups,dc=example,dc=com", 0, "(|(apple-generateduid=right_coast)(apple-generateduid=left_coast))", [])), 2)
-
-
-        def test_ldapRecordCreation(self):
-            """
-            Exercise _ldapResultToRecord(), which converts a dictionary
-            of LDAP attributes into an LdapDirectoryRecord
-            """
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-
-            # User without enabled-for-calendaring specified
-
-            dn = "uid=odtestamanda,cn=users,dc=example,dc=com"
-            guid = '9DC04A70-E6DD-11DF-9492-0800200C9A66'
-            attrs = {
-                'uid': ['odtestamanda'],
-                'apple-generateduid': [guid],
-                'sn': ['Test'],
-                'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                'givenName': ['Amanda'],
-                'cn': ['Amanda Test']
-            }
-
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_users
-            )
-            self.assertEquals(record.guid, guid)
-            self.assertEquals(
-                record.emailAddresses,
-                set(['alternate at example.com', 'odtestamanda at example.com'])
-            )
-            self.assertEquals(record.shortNames, ('odtestamanda',))
-            self.assertEquals(record.fullName, 'Amanda Test')
-            self.assertEquals(record.firstName, 'Amanda')
-            self.assertEquals(record.lastName, 'Test')
-            self.assertEquals(record.serverID, None)
-            self.assertFalse(record.enabledForCalendaring)
-
-            # User with enabled-for-calendaring specified
-
-            dn = "uid=odtestamanda,cn=users,dc=example,dc=com"
-            guid = '9DC04A70-E6DD-11DF-9492-0800200C9A66'
-            attrs = {
-                'uid': ['odtestamanda'],
-                'apple-generateduid': [guid],
-                'enable-calendar': ["yes"],
-                'sn': ['Test'],
-                'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                'givenName': ['Amanda'],
-                'cn': ['Amanda Test']
-            }
-
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_users
-            )
-            self.assertTrue(record.enabledForCalendaring)
-
-            # User with "podding" info
-
-            dn = "uid=odtestamanda,cn=users,dc=example,dc=com"
-            guid = '9DC04A70-E6DD-11DF-9492-0800200C9A66'
-            attrs = {
-                'uid': ['odtestamanda'],
-                'apple-generateduid': [guid],
-                'cn': ['Amanda Test'],
-                'server-id': ["test-server-id"],
-            }
-
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_users
-            )
-            self.assertEquals(record.serverID, "test-server-id")
-
-            # User missing guidAttr
-
-            dn = "uid=odtestamanda,cn=users,dc=example,dc=com"
-            attrs = {
-                'uid': ['odtestamanda'],
-                'cn': ['Amanda Test'],
-            }
-
-            self.assertRaises(
-                MissingGuidException,
-                self.service._ldapResultToRecord, dn, attrs,
-                self.service.recordType_users
-            )
-
-            # User missing record name
-
-            dn = "uid=odtestamanda,cn=users,dc=example,dc=com"
-            attrs = {
-                'apple-generateduid': ['9ABDD881-B3A4-4065-9DA7-12095F40A898'],
-                'cn': ['Amanda Test'],
-            }
-
-            self.assertRaises(
-                MissingRecordNameException,
-                self.service._ldapResultToRecord, dn, attrs,
-                self.service.recordType_users
-            )
-
-            # Group with direct user members and nested group
-
-            dn = "cn=odtestgrouptop,cn=groups,dc=example,dc=com"
-            guid = '6C6CD280-E6E3-11DF-9492-0800200C9A66'
-            attrs = {
-                'apple-generateduid': [guid],
-                'uniqueMember': [
-                    'uid=odtestamanda,cn=users,dc=example,dc=com',
-                    'uid=odtestbetty,cn=users,dc=example,dc=com',
-                    'cn=odtestgroupb,cn=groups,dc=example,dc=com',
-                ],
-                'cn': ['odtestgrouptop']
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_groups
-            )
-            self.assertEquals(record.guid, guid)
-            self.assertEquals(
-                record.memberGUIDs(),
-                set(
-                    [
-                        'cn=odtestgroupb,cn=groups,dc=example,dc=com',
-                        'uid=odtestamanda,cn=users,dc=example,dc=com',
-                        'uid=odtestbetty,cn=users,dc=example,dc=com',
-                    ]
-                )
-            )
-
-            # Group with illegal DN value in members
-
-            dn = "cn=odtestgrouptop,cn=groups,dc=example,dc=com"
-            guid = '6C6CD280-E6E3-11DF-9492-0800200C9A66'
-            attrs = {
-                'apple-generateduid': [guid],
-                'uniqueMember': [
-                    'uid=odtestamanda,cn=users,dc=example,dc=com',
-                    'uid=odtestbetty ,cn=users,dc=example,dc=com',
-                    'cn=odtestgroupb+foo,cn=groups,dc=example,dc=com',
-                ],
-                'cn': ['odtestgrouptop']
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_groups
-            )
-            self.assertEquals(record.guid, guid)
-            self.assertEquals(
-                record.memberGUIDs(),
-                set(
-                    [
-                        'uid=odtestamanda,cn=users,dc=example,dc=com',
-                        'uid=odtestbetty,cn=users,dc=example,dc=com',
-                    ]
-                )
-            )
-
-            # Resource with delegates, autoSchedule = True, and autoAcceptGroup
-
-            dn = "cn=odtestresource,cn=resources,dc=example,dc=com"
-            guid = 'D3094652-344B-4633-8DB8-09639FA00FB6'
-            attrs = {
-                'apple-generateduid': [guid],
-                'cn': ['odtestresource'],
-                'apple-resource-info': ["""<?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>CalendaringDelegate</key>
-<string>6C6CD280-E6E3-11DF-9492-0800200C9A66</string>
-<key>ReadOnlyCalendaringDelegate</key>
-<string>6AA1AE12-592F-4190-A069-547CD83C47C0</string>
-<key>AutoAcceptGroup</key>
-<string>77A8EB52-AA2A-42ED-8843-B2BEE863AC70</string>
-</dict>
-</dict>
-</plist>"""]
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_resources
-            )
-            self.assertEquals(record.guid, guid)
-            self.assertEquals(
-                record.externalProxies(),
-                set(['6C6CD280-E6E3-11DF-9492-0800200C9A66'])
-            )
-            self.assertEquals(
-                record.externalReadOnlyProxies(),
-                set(['6AA1AE12-592F-4190-A069-547CD83C47C0'])
-            )
-            self.assertTrue(record.autoSchedule)
-            self.assertEquals(
-                record.autoAcceptGroup, '77A8EB52-AA2A-42ED-8843-B2BEE863AC70'
-            )
-
-            # Resource with no delegates and autoSchedule = False
-
-            dn = "cn=odtestresource,cn=resources,dc=example,dc=com"
-            guid = 'D3094652-344B-4633-8DB8-09639FA00FB6'
-            attrs = {
-                'apple-generateduid': [guid],
-                'cn': ['odtestresource'],
-                'apple-resource-info': ["""<?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/>
-</dict>
-</dict>
-</plist>"""]
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_resources
-            )
-            self.assertEquals(record.guid, guid)
-            self.assertEquals(record.externalProxies(), set())
-            self.assertEquals(record.externalReadOnlyProxies(), set())
-            self.assertFalse(record.autoSchedule)
-            self.assertEquals(record.autoAcceptGroup, "")
-
-            # Now switch off the resourceInfoAttr and switch to individual
-            # attributes...
-            self.service.resourceSchema = {
-                "resourceInfoAttr": "",
-                "autoScheduleAttr": "auto-schedule",
-                "autoScheduleEnabledValue": "yes",
-                "proxyAttr": "proxy",
-                "readOnlyProxyAttr": "read-only-proxy",
-                "autoAcceptGroupAttr": "auto-accept-group",
-            }
-
-            # Resource with delegates and autoSchedule = True
-
-            dn = "cn=odtestresource,cn=resources,dc=example,dc=com"
-            guid = 'D3094652-344B-4633-8DB8-09639FA00FB6'
-            attrs = {
-                'apple-generateduid': [guid],
-                'cn': ['odtestresource'],
-                'auto-schedule': ['yes'],
-                'proxy': ['6C6CD280-E6E3-11DF-9492-0800200C9A66'],
-                'read-only-proxy': ['6AA1AE12-592F-4190-A069-547CD83C47C0'],
-                'auto-accept-group': ['77A8EB52-AA2A-42ED-8843-B2BEE863AC70'],
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_resources
-            )
-            self.assertEquals(record.guid, guid)
-            self.assertEquals(
-                record.externalProxies(),
-                set(['6C6CD280-E6E3-11DF-9492-0800200C9A66'])
-            )
-            self.assertEquals(
-                record.externalReadOnlyProxies(),
-                set(['6AA1AE12-592F-4190-A069-547CD83C47C0'])
-            )
-            self.assertTrue(record.autoSchedule)
-            self.assertEquals(
-                record.autoAcceptGroup,
-                '77A8EB52-AA2A-42ED-8843-B2BEE863AC70'
-            )
-
-            # Record with lowercase guid
-            dn = "uid=odtestamanda,cn=users,dc=example,dc=com"
-            guid = '9dc04a70-e6dd-11df-9492-0800200c9a66'
-            attrs = {
-                'uid': ['odtestamanda'],
-                'apple-generateduid': [guid],
-                'sn': ['Test'],
-                'mail': ['odtestamanda at example.com', 'alternate at example.com'],
-                'givenName': ['Amanda'],
-                'cn': ['Amanda Test']
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_users
-            )
-            self.assertEquals(record.guid, guid.upper())
-
-            # Location with associated Address
-
-            dn = "cn=odtestlocation,cn=locations,dc=example,dc=com"
-            guid = "D3094652-344B-4633-8DB8-09639FA00FB6"
-            attrs = {
-                "apple-generateduid": [guid],
-                "cn": ["odtestlocation"],
-                "assocAddr": ["6C6CD280-E6E3-11DF-9492-0800200C9A66"],
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_locations
-            )
-            self.assertEquals(record.extras, {
-                "associatedAddress": "6C6CD280-E6E3-11DF-9492-0800200C9A66"
-            })
-
-            # Address with street and geo
-
-            dn = "cn=odtestaddress,cn=buildings,dc=example,dc=com"
-            guid = "6C6CD280-E6E3-11DF-9492-0800200C9A66"
-            attrs = {
-                "apple-generateduid": [guid],
-                "cn": ["odtestaddress"],
-                "coordinates": ["geo:1,2"],
-                "postal": ["1 Infinite Loop, Cupertino, CA"],
-            }
-            record = self.service._ldapResultToRecord(
-                dn, attrs, self.service.recordType_addresses
-            )
-            self.assertEquals(record.extras, {
-                "geo": "geo:1,2",
-                "streetAddress": "1 Infinite Loop, Cupertino, CA",
-            })
-
-        def test_listRecords(self):
-            """
-            listRecords makes an LDAP query (with fake results in this test)
-            and turns the results into records
-            """
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-
-            records = self.service.listRecords(self.service.recordType_users)
-            self.assertEquals(len(records), 4)
-            self.assertEquals(
-                set([r.firstName for r in records]),
-                set(["Amanda", "Betty", "Cyrus", "Wilfredo"])  # Carlene is skipped because no guid in LDAP
-            )
-
-        def test_restrictedPrincipalsUsingDN(self):
-            """
-            If restrictToGroup is in place, restrictedPrincipals should return only the principals
-            within that group.  In this case we're testing scenarios in which membership
-            is specified by DN
-            """
-            for scenario in (
-                self.nestedUsingSameAttributeUsingDN,
-                self.nestedUsingDifferentAttributeUsingDN,
-            ):
-                self.setupService(scenario)
-
-                self.assertEquals(
-                    set(
-                        [
-                            "cn=left_coast,cn=groups,dc=example,dc=com",
-                            "cn=right_coast,cn=groups,dc=example,dc=com",
-                            "uid=cdaboo,cn=users,dc=example,dc=com",
-                            "uid=dreid,cn=users,dc=example,dc=com",
-                            "uid=lecroy,cn=users,dc=example,dc=com",
-                            "uid=wsanchez,cn=users,dc=example,dc=com",
-                        ]
-                    ),
-                    self.service.restrictedPrincipals)
-
-                dn = "uid=cdaboo,cn=users,dc=example,dc=com"
-                attrs = {
-                    'uid': ['cdaboo'],
-                    'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                    'sn': ['Daboo'],
-                    'mail': ['daboo at example.com'],
-                    'givenName': ['Cyrus'],
-                    'cn': ['Cyrus Daboo']
-                }
-                self.assertTrue(self.service.isAllowedByRestrictToGroup(dn, attrs))
-
-                dn = "uid=unknown,cn=users,dc=example,dc=com"
-                attrs = {
-                    'uid': ['unknown'],
-                    'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                    'sn': ['unknown'],
-                    'mail': ['unknown at example.com'],
-                    'givenName': ['unknown'],
-                    'cn': ['unknown']
-                }
-                self.assertFalse(self.service.isAllowedByRestrictToGroup(dn, attrs))
-
-
-        def test_restrictedPrincipalsUsingGUID(self):
-            """
-            If restrictToGroup is in place, restrictedPrincipals should return only the principals
-            within that group.  In this case we're testing scenarios in which membership
-            is specified by an attribute, not DN
-            """
-            for scenario in (
-                self.nestedUsingDifferentAttributeUsingGUID,
-                self.nestedUsingSameAttributeUsingGUID,
-            ):
-                self.setupService(scenario)
-
-                self.assertEquals(
-                    set(
-                        [
-                            "left_coast",
-                            "right_coast",
-                            "5A985493-EE2C-4665-94CF-4DFEA3A89500",
-                            "6423F94A-6B76-4A3A-815B-D52CFD77935D",
-                        ]
-                    ),
-                    self.service.restrictedPrincipals)
-
-                dn = "uid=cdaboo,cn=users,dc=example,dc=com"
-                attrs = {
-                    'uid': ['cdaboo'],
-                    'apple-generateduid': ['5A985493-EE2C-4665-94CF-4DFEA3A89500'],
-                    'sn': ['Daboo'],
-                    'mail': ['daboo at example.com'],
-                    'givenName': ['Cyrus'],
-                    'cn': ['Cyrus Daboo']
-                }
-                self.assertTrue(self.service.isAllowedByRestrictToGroup(dn, attrs))
-
-                dn = "uid=unknown,cn=users,dc=example,dc=com"
-                attrs = {
-                    'uid': ['unknown'],
-                    'apple-generateduid': ['unknown'],
-                    'sn': ['unknown'],
-                    'mail': ['unknown at example.com'],
-                    'givenName': ['unknown'],
-                    'cn': ['unknown']
-                }
-                self.assertFalse(self.service.isAllowedByRestrictToGroup(dn, attrs))
-
-
-        @inlineCallbacks
-        def test_groupMembershipAliases(self):
-            """
-            Exercise a directory environment where group membership does not refer
-            to guids but instead uses LDAP DNs.  This example uses the LDAP attribute
-            "uniqueMember" to specify members of a group.  The value of this attribute
-            is each members' DN.  Even though the proxy database deals strictly in
-            guids, updateCache( ) is smart enough to map between guids and this
-            attribute which is referred to in the code as record.cachedGroupsAlias().
-            """
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-
-            # Set up proxydb and preload it from xml
-            calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
-            yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
-
-            # Set up the GroupMembershipCache
-            cache = GroupMembershipCache("ProxyDB", expireSeconds=60)
-            self.service.groupMembershipCache = cache
-            updater = GroupMembershipCacheUpdater(
-                calendaruserproxy.ProxyDBService,
-                self.service, 30, 15, 30, cache=cache, useExternalProxies=False
-            )
-
-            self.assertEquals((False, 8, 8), (yield updater.updateCache()))
-
-            users = self.service.recordType_users
-
-            for shortName, groups in [
-                ("cdaboo", set(["both_coasts", "recursive1_coasts", "recursive2_coasts"])),
-                ("wsanchez", set(["both_coasts", "left_coast", "recursive1_coasts", "recursive2_coasts"])),
-            ]:
-
-                record = self.service.recordWithShortName(users, shortName)
-                self.assertEquals(groups, (yield record.cachedGroups()))
-
-
-        def test_getExternalProxyAssignments(self):
-            """
-            Verify getExternalProxyAssignments can extract assignments from the
-            directory, and that guids are normalized.
-            """
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-            self.assertEquals(
-                self.service.getExternalProxyAssignments(),
-                [
-                    ('D91B21B9-B856-495A-8E36-0E5AD54EFB3A#calendar-proxy-read',
-                        ['5A985493-EE2C-4665-94CF-4DFEA3A89500']),
-                    ('D91B21B9-B856-495A-8E36-0E5AD54EFB3A#calendar-proxy-write',
-                        ['6423F94A-6B76-4A3A-815B-D52CFD77935D']),
-                    ('753E5A60-AFFD-45E4-BF2C-31DAB459353F#calendar-proxy-write',
-                        ['6423F94A-6B76-4A3A-815B-D52CFD77935D'])
-                ]
-            )
-
-        def test_splitIntoBatches(self):
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-            # Data is perfect multiple of size
-            results = list(splitIntoBatches(set(range(12)), 4))
-            self.assertEquals(
-                results,
-                [set([0, 1, 2, 3]), set([4, 5, 6, 7]), set([8, 9, 10, 11])]
-            )
-
-            # Some left overs
-            results = list(splitIntoBatches(set(range(12)), 5))
-            self.assertEquals(
-                results,
-                [set([0, 1, 2, 3, 4]), set([8, 9, 5, 6, 7]), set([10, 11])]
-            )
-
-            # Empty
-            results = list(splitIntoBatches(set([]), 5))  # empty data
-            self.assertEquals(results, [set([])])
-
-        def test_recordTypeForDN(self):
-            # Ensure dn comparison is case insensitive and ignores extra
-            # whitespace
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-
-            # Base DNs for each recordtype should already be lowercase
-            for dn in self.service.typeDNs.itervalues():
-                dnStr = ldap.dn.dn2str(dn)
-                self.assertEquals(dnStr, dnStr.lower())
-
-            # Match
-            dnStr = "uid=foo,cn=USers ,dc=EXAMple,dc=com"
-            self.assertEquals(self.service.recordTypeForDN(dnStr), "users")
-            dnStr = "uid=foo,cn=PLaces,dc=EXAMple,dc=com"
-            self.assertEquals(self.service.recordTypeForDN(dnStr), "locations")
-            dnStr = "uid=foo,cn=Groups  ,dc=EXAMple,dc=com"
-            self.assertEquals(self.service.recordTypeForDN(dnStr), "groups")
-            dnStr = "uid=foo,cn=Resources  ,dc=EXAMple,dc=com"
-            self.assertEquals(self.service.recordTypeForDN(dnStr), "resources")
-
-            # No Match
-            dnStr = "uid=foo,cn=US ers ,dc=EXAMple,dc=com"
-            self.assertEquals(self.service.recordTypeForDN(dnStr), None)
-
-        def test_normalizeDN(self):
-            for input, expected in (
-                ("uid=foo,cn=users,dc=example,dc=com",
-                 "uid=foo,cn=users,dc=example,dc=com"),
-                ("uid=FoO,cn=uSeRs,dc=ExAmPlE,dc=CoM",
-                 "uid=foo,cn=users,dc=example,dc=com"),
-                ("uid=FoO , cn=uS eRs , dc=ExA mPlE ,   dc=CoM",
-                 "uid=foo,cn=us ers,dc=exa mple,dc=com"),
-                ("uid=FoO , cn=uS  eRs , dc=ExA    mPlE ,   dc=CoM",
-                 "uid=foo,cn=us ers,dc=exa mple,dc=com"),
-            ):
-                self.assertEquals(expected, normalizeDNstr(input))
-
-        def test_queryDirectory(self):
-            """
-            Verify queryDirectory skips LDAP queries where there has been no
-            LDAP attribute mapping provided for the given index type.
-            """
-            self.setupService(self.nestedUsingDifferentAttributeUsingDN)
-
-            self.history = []
-
-            def stubSearchMethod(base, scope, filterstr="(objectClass=*)",
-                                 attrlist=None, timeoutSeconds=-1,
-                                 resultLimit=0):
-                self.history.append((base, scope, filterstr))
-
-            recordTypes = [
-                self.service.recordType_users,
-                self.service.recordType_groups,
-                self.service.recordType_locations,
-                self.service.recordType_resources,
-            ]
-            self.service.queryDirectory(
-                recordTypes,
-                self.service.INDEX_TYPE_CUA,
-                "mailto:test at example.com",
-                queryMethod=stubSearchMethod
-            )
-            self.assertEquals(
-                self.history,
-                [('cn=users,dc=example,dc=com', 2, '(&(!(objectClass=organizationalUnit))(|(mail=test at example.com)(emailAliases=test at example.com)))'), ('cn=groups,dc=example,dc=com', 2, '(&(!(objectClass=organizationalUnit))(|(mail=test at example.com)(emailAliases=test at example.com)))')]
-            )

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_livedirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_livedirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_livedirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,209 +0,0 @@
-##
-# Copyright (c) 2011-2014 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 print_function
-
-runLDAPTests = False
-runODTests = False
-
-try:
-    import ldap
-    import socket
-
-    testServer = "localhost"
-    base = ",".join(["dc=%s" % (p,) for p in socket.gethostname().split(".")])
-    print("Using base: %s" % (base,))
-
-    try:
-        cxn = ldap.open(testServer)
-        results = cxn.search_s(base, ldap.SCOPE_SUBTREE, "(uid=odtestamanda)",
-            ["cn"])
-        if len(results) == 1:
-            runLDAPTests = True
-    except ldap.LDAPError:
-        pass # Don't run live tests
-
-except ImportError:
-    print("Could not import ldap module (skipping LDAP tests)")
-
-try:
-    from calendarserver.platform.darwin.od import opendirectory, dsattributes
-
-    directory = opendirectory.odInit("/Search")
-
-    results = list(opendirectory.queryRecordsWithAttribute_list(
-        directory,
-        dsattributes.kDS1AttrGeneratedUID,
-        "9DC04A70-E6DD-11DF-9492-0800200C9A66",
-        dsattributes.eDSExact,
-        False,
-        dsattributes.kDSStdRecordTypeUsers,
-        None,
-        count=0
-    ))
-    recordNames = [x[0] for x in results]
-    if "odtestamanda" in recordNames:
-        runODTests = True
-    else:
-        print("Test OD records not found (skipping OD tests)")
-
-except ImportError:
-    print("Could not import OpenDirectory framework (skipping OD tests)")
-
-
-if runLDAPTests or runODTests:
-
-    from twistedcaldav.test.util import TestCase
-    from twistedcaldav.directory import augment
-    from twistedcaldav.directory.test.test_xmlfile import augmentsFile
-    from twisted.internet.defer import inlineCallbacks
-
-    class LiveDirectoryTests(object):
-
-        def test_ldapRecordWithShortName(self):
-            record = self.svc.recordWithShortName("users", "odtestamanda")
-            self.assertTrue(record is not None)
-
-        def test_ldapRecordWithGUID(self):
-            record = self.svc.recordWithGUID("9DC04A70-E6DD-11DF-9492-0800200C9A66")
-            self.assertTrue(record is not None)
-
-        @inlineCallbacks
-        def test_ldapRecordsMatchingFields(self):
-            fields = (
-                ("firstName", "Amanda", True, "exact"),
-                ("lastName", "Te", True, "starts-with"),
-            )
-            records = list(
-                (yield self.svc.recordsMatchingFields(fields, operand="and"))
-            )
-            self.assertEquals(1, len(records))
-            record = self.svc.recordWithGUID("9DC04A70-E6DD-11DF-9492-0800200C9A66")
-            self.assertEquals(records, [record])
-
-        @inlineCallbacks
-        def test_restrictToGroup(self):
-            self.svc.restrictEnabledRecords = True
-            self.svc.restrictToGroup = "odtestgrouptop"
-
-            # Faulting in specific records will return records outside of
-            # the restrictToGroup, but they won't be enabledForCalendaring
-            # and AddressBooks:
-
-            # Amanda is a direct member of that group
-            record = self.svc.recordWithShortName("users", "odtestamanda")
-            self.assertTrue(record.enabledForCalendaring)
-            self.assertTrue(record.enabledForAddressBooks)
-
-            # Betty is a direct member of that group
-            record = self.svc.recordWithShortName("users", "odtestbetty")
-            self.assertTrue(record.enabledForCalendaring)
-            self.assertTrue(record.enabledForAddressBooks)
-
-            # Carlene is in a nested group
-            record = self.svc.recordWithShortName("users", "odtestcarlene")
-            self.assertTrue(record.enabledForCalendaring)
-            self.assertTrue(record.enabledForAddressBooks)
-
-            # Denise is not in the group
-            record = self.svc.recordWithShortName("users", "odtestdenise")
-            self.assertFalse(record.enabledForCalendaring)
-            self.assertFalse(record.enabledForAddressBooks)
-
-            # Searching for records using principal-property-search will not
-            # yield records outside of the restrictToGroup:
-
-            fields = (
-                ("lastName", "Test", True, "exact"),
-            )
-            records = list(
-                (yield self.svc.recordsMatchingFields(fields))
-            )
-            self.assertEquals(3, len(records))
-
-            # These two are directly in the restrictToGroup:
-            record = self.svc.recordWithShortName("users", "odtestamanda")
-            self.assertTrue(record in records)
-            record = self.svc.recordWithShortName("users", "odtestbetty")
-            self.assertTrue(record in records)
-            # Carlene is still picked up because she is in a nested group
-            record = self.svc.recordWithShortName("users", "odtestcarlene")
-            self.assertTrue(record in records)
-
-    if runLDAPTests:
-
-        from twistedcaldav.directory.ldapdirectory import LdapDirectoryService
-        print("Running live LDAP tests against %s" % (testServer,))
-
-        class LiveLDAPDirectoryServiceCase(LiveDirectoryTests, TestCase):
-
-            def setUp(self):
-                params = {
-                    "augmentService":
-                        augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-                    "uri": "ldap://%s" % (testServer,),
-                    "rdnSchema": {
-                        "base": base,
-                        "guidAttr": "apple-generateduid",
-                        "users": {
-                            "rdn": "cn=users",
-                            "attr": "uid", # used only to synthesize email address
-                            "emailSuffix": None, # used only to synthesize email address
-                            "filter": None, # additional filter for this type
-                            "loginEnabledAttr" : "", # attribute controlling login
-                            "loginEnabledValue" : "yes", # "True" value of above attribute
-                            "mapping" : { # maps internal record names to LDAP
-                                "recordName": "uid",
-                                "fullName" : "cn",
-                                "emailAddresses" : ["mail"], # multiple LDAP fields supported
-                                "firstName" : "givenName",
-                                "lastName" : "sn",
-                            },
-                        },
-                        "groups": {
-                            "rdn": "cn=groups",
-                            "attr": "cn", # used only to synthesize email address
-                            "emailSuffix": None, # used only to synthesize email address
-                            "filter": None, # additional filter for this type
-                            "mapping" : { # maps internal record names to LDAP
-                                "recordName": "cn",
-                                "fullName" : "cn",
-                                "emailAddresses" : ["mail"], # multiple LDAP fields supported
-                                "firstName" : "givenName",
-                                "lastName" : "sn",
-                            },
-                        },
-                    },
-                    "groupSchema": {
-                        "membersAttr": "apple-group-memberguid", # how members are specified
-                        "nestedGroupsAttr" : "apple-group-nestedgroup", # how nested groups are specified
-                        "memberIdAttr": "apple-generateduid", # which attribute the above refers to
-                    },
-                }
-                self.svc = LdapDirectoryService(params)
-
-    if runODTests:
-
-        from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
-        print("Running live OD tests")
-
-        class LiveODDirectoryServiceCase(LiveDirectoryTests, TestCase):
-
-            def setUp(self):
-                params = {
-                    "augmentService":
-                        augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-                }
-                self.svc = OpenDirectoryService(params)

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_modify.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_modify.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_modify.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,159 +0,0 @@
-##
-# Copyright (c) 2005-2014 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 twistedcaldav.config import config
-from twistedcaldav.test.util import TestCase
-from calendarserver.tools.util import getDirectory
-from twext.python.filepath import CachingFilePath as FilePath
-from twistedcaldav.directory.directory import DirectoryError
-
-
-class ModificationTestCase(TestCase):
-
-    def setUp(self):
-        super(ModificationTestCase, self).setUp()
-
-        testRoot = os.path.join(os.path.dirname(__file__), "modify")
-        #configFileName = os.path.join(testRoot, "caldavd.plist")
-        #config.load(configFileName)
-
-        usersFile = os.path.join(testRoot, "users-groups.xml")
-        config.DirectoryService.params.xmlFile = usersFile
-
-        # Copy xml file containing locations/resources to a temp file because
-        # we're going to be modifying it during testing
-
-        origResourcesFile = FilePath(os.path.join(os.path.dirname(__file__),
-            "modify", "resources-locations.xml"))
-        copyResourcesFile = FilePath(self.mktemp())
-        origResourcesFile.copyTo(copyResourcesFile)
-        config.ResourceService.params.xmlFile = copyResourcesFile
-        config.ResourceService.Enabled = True
-
-        augmentsFile = os.path.join(testRoot, "augments.xml")
-        config.AugmentService.params.xmlFiles = (augmentsFile,)
-
-
-    def test_createRecord(self):
-        directory = getDirectory()
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record, None)
-
-        directory.createRecord("resources", guid="resource01",
-            shortNames=("resource01",), uid="resource01",
-            emailAddresses=("res1 at example.com", "res2 at example.com"),
-            comment="Test Comment")
-
-        record = directory.recordWithUID("resource01")
-        self.assertNotEquals(record, None)
-
-        self.assertEquals(len(record.emailAddresses), 2)
-        self.assertEquals(record.extras['comment'], "Test Comment")
-
-        directory.createRecord("resources", guid="resource02", shortNames=("resource02",), uid="resource02")
-
-        record = directory.recordWithUID("resource02")
-        self.assertNotEquals(record, None)
-
-        # Make sure old records are still there:
-        record = directory.recordWithUID("resource01")
-        self.assertNotEquals(record, None)
-        record = directory.recordWithUID("location01")
-        self.assertNotEquals(record, None)
-
-
-    def test_destroyRecord(self):
-        directory = getDirectory()
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record, None)
-
-        directory.createRecord("resources", guid="resource01", shortNames=("resource01",), uid="resource01")
-
-        record = directory.recordWithUID("resource01")
-        self.assertNotEquals(record, None)
-
-        directory.destroyRecord("resources", guid="resource01")
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record, None)
-
-        # Make sure old records are still there:
-        record = directory.recordWithUID("location01")
-        self.assertNotEquals(record, None)
-
-
-    def test_updateRecord(self):
-        directory = getDirectory()
-
-        directory.createRecord("resources", guid="resource01",
-            shortNames=("resource01",), uid="resource01",
-            fullName="Resource number 1")
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record.fullName, "Resource number 1")
-
-        directory.updateRecord("resources", guid="resource01",
-            shortNames=("resource01", "r01"), uid="resource01",
-            fullName="Resource #1", firstName="First", lastName="Last",
-            emailAddresses=("resource01 at example.com", "r01 at example.com"),
-            comment="Test Comment")
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record.fullName, "Resource #1")
-        self.assertEquals(record.firstName, "First")
-        self.assertEquals(record.lastName, "Last")
-        self.assertEquals(set(record.shortNames), set(["resource01", "r01"]))
-        self.assertEquals(record.emailAddresses,
-            set(["resource01 at example.com", "r01 at example.com"]))
-        self.assertEquals(record.extras['comment'], "Test Comment")
-
-        # Make sure old records are still there:
-        record = directory.recordWithUID("location01")
-        self.assertNotEquals(record, None)
-
-
-    def test_createDuplicateRecord(self):
-        directory = getDirectory()
-
-        directory.createRecord("resources", guid="resource01", shortNames=("resource01",), uid="resource01")
-        self.assertRaises(DirectoryError, directory.createRecord, "resources", guid="resource01", shortNames=("resource01",), uid="resource01")
-
-
-    def test_missingShortNames(self):
-        directory = getDirectory()
-
-        directory.createRecord("resources", guid="resource01")
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record.shortNames[0], "resource01")
-
-        directory.updateRecord("resources", guid="resource01",
-            fullName="Resource #1")
-
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record.shortNames[0], "resource01")
-        self.assertEquals(record.fullName, "Resource #1")
-
-
-    def test_missingGUID(self):
-        directory = getDirectory()
-
-        record = directory.createRecord("resources")
-
-        self.assertEquals(record.shortNames[0], record.guid)

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,498 +0,0 @@
-##
-# Copyright (c) 2005-2014 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
-except ImportError:
-    pass
-else:
-    from calendarserver.platform.darwin.od import dsattributes
-    from collections import defaultdict
-    from txweb2.auth.digest import DigestedCredentials
-    from twisted.internet.defer import inlineCallbacks
-    from twisted.python.runtime import platform
-    from twisted.trial.unittest import SkipTest
-    from twistedcaldav.directory import augment
-    from twistedcaldav.directory.appleopendirectory import OpenDirectoryRecord
-    from twistedcaldav.directory.directory import DirectoryService
-    from txdav.common.datastore.test.util import deriveValue, withSpecialValue
-    import twistedcaldav.directory.test.util
-
-    class DigestAuthModule(object):
-        """
-        Stand-in for either configurable OD module, that verifies the response
-        according to its '.response' attribute, set by the test.
-        """
-        class ODError(Exception):
-            pass
-
-        def odInit(self, node):
-            return self
-
-        def authenticateUserDigest(self, directory, node, user, challenge,
-                                   response, method):
-            val = (response == self.response)
-            return val
-
-
-    # Wonky hack to prevent unclean reactor shutdowns
-    class DummyReactor(object):
-        @staticmethod
-        def callLater(*args):
-            pass
-
-    twistedcaldav.directory.appleopendirectory.reactor = DummyReactor
-
-    class OpenDirectory (
-        twistedcaldav.directory.test.util.BasicTestCase,
-        twistedcaldav.directory.test.util.DigestTestCase
-    ):
-        """
-        Test Open Directory directory implementation.
-        """
-        if not platform.isMacOSX():
-            skip = "Currently, OpenDirectory backend only works on MacOS X."
-        recordTypes = set((
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-        ))
-
-        users = groups = {}
-
-        def setUp(self):
-            super(OpenDirectory, self).setUp()
-            try:
-                self._service = OpenDirectoryService(
-                    {
-                        "node" : "/Search",
-                        "augmentService": augment.AugmentXMLDB(xmlFiles=()),
-                    },
-                    odModule=deriveValue(self, "odModule", lambda self: None)
-                )
-            except ImportError, e:
-                raise SkipTest("OpenDirectory module is not available: %s" % (e,))
-
-        def service(self):
-            return self._service
-
-        def test_fullNameNone(self):
-            record = OpenDirectoryRecord(
-                service=self.service(),
-                recordType=DirectoryService.recordType_users,
-                guid="B1F93EB1-DA93-4772-9141-81C250DA36C2",
-                nodeName="/LDAPv2/127.0.0.1",
-                shortNames=("user",),
-                authIDs=set(),
-                fullName=None,
-                firstName="Some",
-                lastName="User",
-                emailAddresses=set(("someuser at example.com",)),
-                memberGUIDs=[],
-                nestedGUIDs=[],
-                extProxies=[],
-                extReadOnlyProxies=[],
-            )
-            self.assertEquals(record.fullName, "")
-
-
-        @withSpecialValue("odModule", DigestAuthModule())
-        def test_invalidODDigest(self):
-            record = OpenDirectoryRecord(
-                service=self.service(),
-                recordType=DirectoryService.recordType_users,
-                guid="B1F93EB1-DA93-4772-9141-81C250DA35B3",
-                nodeName="/LDAPv2/127.0.0.1",
-                shortNames=("user",),
-                authIDs=set(),
-                fullName="Some user",
-                firstName="Some",
-                lastName="User",
-                emailAddresses=set(("someuser at example.com",)),
-                memberGUIDs=[],
-                nestedGUIDs=[],
-                extProxies=[],
-                extReadOnlyProxies=[],
-            )
-
-            digestFields = defaultdict(lambda: "...")
-            digested = DigestedCredentials("user", "GET", "example.com",
-                                           digestFields)
-            od = deriveValue(self, "odModule", lambda x: None)
-            od.response = "invalid"
-
-            self.assertFalse(record.verifyCredentials(digested))
-
-
-        @withSpecialValue("odModule", DigestAuthModule())
-        def test_validODDigest(self):
-            record = OpenDirectoryRecord(
-                service=self.service(),
-                recordType=DirectoryService.recordType_users,
-                guid="B1F93EB1-DA93-4772-9141-81C250DA35B3",
-                nodeName="/LDAPv2/127.0.0.1",
-                shortNames=("user",),
-                authIDs=set(),
-                fullName="Some user",
-                firstName="Some",
-                lastName="User",
-                emailAddresses=set(("someuser at example.com",)),
-                memberGUIDs=[],
-                nestedGUIDs=[],
-                extProxies=[],
-                extReadOnlyProxies=[],
-            )
-
-            digestFields = {
-                "username": "user",
-                "realm": "/Search",
-                "nonce": "ABC",
-                "uri": "/",
-                "response": "123",
-                "algorithm": "md5",
-            }
-            od = deriveValue(self, "odModule", lambda self: None)
-            od.response = (
-                'Digest username="%(username)s", '
-                'realm="%(realm)s", '
-                'nonce="%(nonce)s", '
-                'uri="%(uri)s", '
-                'response="%(response)s",'
-                'algorithm=%(algorithm)s'
-            ) % digestFields
-
-            digested = DigestedCredentials("user", "GET", "example.com",
-                                           digestFields)
-
-            self.assertTrue(record.verifyCredentials(digested))
-
-            # This should be defaulted
-            del digestFields["algorithm"]
-
-            self.assertTrue(record.verifyCredentials(digested))
-
-        def test_queryDirectorySingleGUID(self):
-            """ Test for lookup on existing and non-existing GUIDs """
-
-            def lookupMethod(obj, attr, value, matchType, casei, recordTypes, attributes, count=0):
-
-                data = {
-                    "dsRecTypeStandard:Users" : [
-                        {
-                            dsattributes.kDS1AttrGeneratedUID : "1234567890",
-                            dsattributes.kDSNAttrRecordName : ["user1", "User 1"],
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                        },
-                    ],
-                    "dsRecTypeStandard:Groups" : [],
-                }
-                results = []
-                for recordType in recordTypes:
-                    for entry in data[recordType]:
-                        if entry[attr] == value:
-                            results.append(("", entry))
-                return results
-
-            recordTypes = [DirectoryService.recordType_users, DirectoryService.recordType_groups]
-            self.service().queryDirectory(recordTypes, self.service().INDEX_TYPE_GUID, "1234567890", lookupMethod=lookupMethod)
-            self.assertTrue(self.service().recordWithGUID("1234567890"))
-            self.assertFalse(self.service().recordWithGUID("987654321"))
-
-
-        def test_queryDirectoryDuplicateGUIDs(self):
-            """ Test for lookup on duplicate GUIDs, ensuring they don't get
-                faulted in """
-
-            def lookupMethod(obj, attr, value, matchType, casei, recordType, attributes, count=0):
-
-                data = [
-                    {
-                        dsattributes.kDS1AttrGeneratedUID : "1234567890",
-                        dsattributes.kDSNAttrRecordName : ["user1", "User 1"],
-                        dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                    },
-                    {
-                        dsattributes.kDS1AttrGeneratedUID : "1234567890",
-                        dsattributes.kDSNAttrRecordName : ["user2", "User 2"],
-                        dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                    },
-                ]
-                results = []
-                for entry in data:
-                    if entry[attr] == value:
-                        results.append(("", entry))
-                return results
-
-            recordTypes = [DirectoryService.recordType_users, DirectoryService.recordType_groups]
-            self.service().queryDirectory(recordTypes, self.service().INDEX_TYPE_GUID, "1234567890", lookupMethod=lookupMethod)
-            self.assertFalse(self.service().recordWithGUID("1234567890"))
-
-        def test_queryDirectoryLocalUsers(self):
-            """ Test for lookup on local users, ensuring they do get
-                faulted in """
-
-            def lookupMethod(obj, attr, value, matchType, casei, recordTypes, attributes, count=0):
-                data = {
-                    "dsRecTypeStandard:Users" : [
-                        {
-                            dsattributes.kDS1AttrGeneratedUID : "1234567890",
-                            dsattributes.kDSNAttrRecordName : ["user1", "User 1"],
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                            dsattributes.kDSNAttrMetaNodeLocation : "/Local/Default",
-                        },
-                        {
-                            dsattributes.kDS1AttrGeneratedUID : "987654321",
-                            dsattributes.kDSNAttrRecordName : ["user2", "User 2"],
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-                        },
-                    ],
-                    "dsRecTypeStandard:Groups" : [],
-                }
-                results = []
-                for recordType in recordTypes:
-                    for entry in data[recordType]:
-                        if entry[attr] == value:
-                            results.append(("", entry))
-                return results
-
-            recordTypes = [DirectoryService.recordType_users, DirectoryService.recordType_groups]
-            self.service().queryDirectory(recordTypes, self.service().INDEX_TYPE_GUID, "1234567890", lookupMethod=lookupMethod)
-            self.service().queryDirectory(recordTypes, self.service().INDEX_TYPE_GUID, "987654321", lookupMethod=lookupMethod)
-            self.assertTrue(self.service().recordWithGUID("1234567890"))
-            self.assertTrue(self.service().recordWithGUID("987654321"))
-
-        def test_queryDirectoryEmailAddresses(self):
-            """ Test to ensure we only ask for users when email address is
-                part of the query """
-
-            def lookupMethod(obj, attr, value, matchType, casei, recordType, attributes, count=0):
-
-                if recordType != ['dsRecTypeStandard:Users']:
-                    raise ValueError
-
-                return []
-
-            recordTypes = [DirectoryService.recordType_users, DirectoryService.recordType_groups]
-            self.service().queryDirectory(recordTypes, self.service().INDEX_TYPE_CUA, "mailto:user1 at example.com", lookupMethod=lookupMethod)
-
-
-        @inlineCallbacks
-        def test_recordsMatchingFields(self):
-
-
-            def lookupMethod(obj, attribute, value, matchType, caseless,
-                recordTypes, attributes):
-
-                data = {
-                    dsattributes.kDSStdRecordTypeUsers : (
-                        {
-                            dsattributes.kDS1AttrDistinguishedName : "Morgen Sagen",
-                            dsattributes.kDSNAttrRecordName : "morgen",
-                            dsattributes.kDS1AttrFirstName : "Morgen",
-                            dsattributes.kDS1AttrLastName : "Sagen",
-                            dsattributes.kDSNAttrEMailAddress : "morgen at example.com",
-                            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-                            dsattributes.kDS1AttrGeneratedUID : "83479230-821E-11DE-B6B0-DBB02C6D659D",
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                        },
-                        {
-                            dsattributes.kDS1AttrDistinguishedName : "Morgan Sagan",
-                            dsattributes.kDSNAttrRecordName : "morgan",
-                            dsattributes.kDS1AttrFirstName : "Morgan",
-                            dsattributes.kDS1AttrLastName : "Sagan",
-                            dsattributes.kDSNAttrEMailAddress : "morgan at example.com",
-                            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-                            dsattributes.kDS1AttrGeneratedUID : "93479230-821E-11DE-B6B0-DBB02C6D659D",
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                        },
-                        {
-                            dsattributes.kDS1AttrDistinguishedName : "Shari Sagen",
-                            dsattributes.kDSNAttrRecordName : "shari",
-                            dsattributes.kDS1AttrFirstName : "Shari",
-                            dsattributes.kDS1AttrLastName : "Sagen",
-                            dsattributes.kDSNAttrEMailAddress : "shari at example.com",
-                            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-                            dsattributes.kDS1AttrGeneratedUID : "A3479230-821E-11DE-B6B0-DBB02C6D659D",
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                        },
-                        {
-                            dsattributes.kDS1AttrDistinguishedName : "Local Morgen",
-                            dsattributes.kDSNAttrRecordName : "localmorgen",
-                            dsattributes.kDS1AttrFirstName : "Local",
-                            dsattributes.kDS1AttrLastName : "Morgen",
-                            dsattributes.kDSNAttrEMailAddress : "localmorgen at example.com",
-                            dsattributes.kDSNAttrMetaNodeLocation : "/Local/Default",
-                            dsattributes.kDS1AttrGeneratedUID : "B3479230-821E-11DE-B6B0-DBB02C6D659D",
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeUsers,
-                        },
-                    ),
-                    dsattributes.kDSStdRecordTypeGroups : (
-                        {
-                            dsattributes.kDS1AttrDistinguishedName : "Test Group",
-                            dsattributes.kDSNAttrRecordName : "testgroup",
-                            dsattributes.kDS1AttrFirstName : None,
-                            dsattributes.kDS1AttrLastName : None,
-                            dsattributes.kDSNAttrEMailAddress : None,
-                            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-                            dsattributes.kDS1AttrGeneratedUID : "C3479230-821E-11DE-B6B0-DBB02C6D659D",
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeGroups,
-                        },
-                        {
-                            dsattributes.kDS1AttrDistinguishedName : "Morgen's Group",
-                            dsattributes.kDSNAttrRecordName : "morgensgroup",
-                            dsattributes.kDS1AttrFirstName : None,
-                            dsattributes.kDS1AttrLastName : None,
-                            dsattributes.kDSNAttrEMailAddress : None,
-                            dsattributes.kDSNAttrMetaNodeLocation : "/LDAPv3/127.0.0.1",
-                            dsattributes.kDS1AttrGeneratedUID : "D3479230-821E-11DE-B6B0-DBB02C6D659D",
-                            dsattributes.kDSNAttrRecordType : dsattributes.kDSStdRecordTypeGroups,
-                        },
-                    ),
-                    dsattributes.kDSStdRecordTypePlaces : (),
-                    dsattributes.kDSStdRecordTypeResources : (),
-                }
-
-                def attributeMatches(fieldValue, value, caseless, matchType):
-                    if fieldValue is None:
-                        return False
-                    if caseless:
-                        fieldValue = fieldValue.lower()
-                        value = value.lower()
-                    if matchType == dsattributes.eDSStartsWith:
-                        if fieldValue.startswith(value):
-                            return True
-                    elif matchType == dsattributes.eDSContains:
-                        try:
-                            fieldValue.index(value)
-                            return True
-                        except ValueError:
-                            pass
-                    else: # exact
-                        if fieldValue == value:
-                            return True
-                    return False
-
-                results = []
-                for recordType in recordTypes:
-                    for row in data[recordType]:
-                        if attributeMatches(row[attribute], value, caseless,
-                            matchType):
-                            results.append((row[dsattributes.kDSNAttrRecordName], row))
-
-                return results
-
-            #
-            # OR
-            #
-            fields = [
-                ("fullName", "mor", True, u"starts-with"),
-                ("emailAddresses", "mor", True, u"starts-with"),
-                ("firstName", "mor", True, u"starts-with"),
-                ("lastName", "mor", True, u"starts-with"),
-            ]
-
-            # any record type
-            results = (yield self.service().recordsMatchingFields(
-                fields,
-                lookupMethod=lookupMethod
-            ))
-            results = list(results)
-            self.assertEquals(len(results), 4)
-            for record in results:
-                self.assertTrue(isinstance(record, OpenDirectoryRecord))
-
-            # just users
-            results = (yield self.service().recordsMatchingFields(fields,
-                recordType="users",
-                lookupMethod=lookupMethod))
-            results = list(results)
-            self.assertEquals(len(results), 3)
-
-            # just groups
-            results = (yield self.service().recordsMatchingFields(fields,
-                recordType="groups",
-                lookupMethod=lookupMethod))
-            results = list(results)
-            self.assertEquals(len(results), 1)
-
-            #
-            # AND
-            #
-            fields = [
-                ("firstName", "morgen", True, u"equals"),
-                ("lastName", "age", True, u"contains")
-            ]
-            results = (yield self.service().recordsMatchingFields(fields,
-                operand="and", lookupMethod=lookupMethod))
-            results = list(results)
-            self.assertEquals(len(results), 1)
-
-            #
-            # case sensitivity
-            #
-            fields = [
-                ("firstName", "morgen", False, u"equals"),
-            ]
-            results = (yield self.service().recordsMatchingFields(fields,
-                lookupMethod=lookupMethod))
-            results = list(results)
-            self.assertEquals(len(results), 0)
-
-            fields = [
-                ("firstName", "morgen", True, u"equals"),
-            ]
-            results = (yield self.service().recordsMatchingFields(
-                fields,
-                lookupMethod=lookupMethod
-            ))
-            results = list(results)
-            self.assertEquals(len(results), 1)
-
-            #
-            # no matches
-            #
-            fields = [
-                ("firstName", "xyzzy", True, u"starts-with"),
-                ("lastName", "plugh", True, u"contains")
-            ]
-            results = (yield self.service().recordsMatchingFields(
-                fields,
-                operand="and",
-                lookupMethod=lookupMethod
-            ))
-            results = list(results)
-            self.assertEquals(len(results), 0)
-
-
-    class OpenDirectorySubset (OpenDirectory):
-        """
-        Test the recordTypes subset feature of Apple OpenDirectoryService.
-        """
-        recordTypes = set((
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-        ))
-
-        def setUp(self):
-            super(OpenDirectorySubset, self).setUp()
-            self._service = OpenDirectoryService(
-                {
-                    "node" : "/Search",
-                    "recordTypes" : (DirectoryService.recordType_users, DirectoryService.recordType_groups),
-                    "augmentService" : augment.AugmentXMLDB(xmlFiles=()),
-                },
-                odModule=deriveValue(self, "odModule", lambda x: None)
-            )

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectorybacker.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectorybacker.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_opendirectorybacker.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,55 +0,0 @@
-##
-# Copyright (c) 2011-2014 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.opendirectorybacker import VCardRecord
-except ImportError:
-    pass
-else:
-    from twistedcaldav.test.util import TestCase
-
-    class VCardRecordTestCase(TestCase):
-
-
-        def test_multiplePhoneNumbersAndEmailAddresses(self):
-            attributes = {
-                u'dsAttrTypeStandard:AppleMetaRecordName': ['uid=odtestamanda,cn=users,dc=dalek,dc=example,dc=com'],
-                u'dsAttrTypeStandard:ModificationTimestamp': '20111017170937Z',
-                u'dsAttrTypeStandard:PhoneNumber': ['408 555-1212', '415 555-1212'],
-                u'dsAttrTypeStandard:RecordType': ['dsRecTypeStandard:Users'],
-                u'dsAttrTypeStandard:AppleMetaNodeLocation': ['/LDAPv3/127.0.0.1'],
-                u'dsAttrTypeStandard:RecordName': ['odtestamanda'],
-                u'dsAttrTypeStandard:FirstName': 'Amanda',
-                u'dsAttrTypeStandard:GeneratedUID': '9DC04A70-E6DD-11DF-9492-0800200C9A66',
-                u'dsAttrTypeStandard:LastName': 'Test',
-                u'dsAttrTypeStandard:CreationTimestamp': '20110927182945Z',
-                u'dsAttrTypeStandard:EMailAddress': ['amanda at example.com', 'second at example.com'],
-                u'dsAttrTypeStandard:RealName': 'Amanda Test',
-            }
-            vcardRecord = VCardRecord(StubService(), attributes)
-            vcard = vcardRecord.vCard()
-            properties = set([prop.value() for prop in vcard.properties("TEL")])
-            self.assertEquals(properties, set(["408 555-1212", "415 555-1212"]))
-            properties = set([prop.value() for prop in vcard.properties("EMAIL")])
-            self.assertEquals(properties, set(["amanda at example.com", "second at example.com"]))
-
-
-
-    class StubService(object):
-        addDSAttrXProperties = False
-        directoryBackedAddressBook = None
-        appleInternalServer = False
-        realmName = "testing"

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_principal.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_principal.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_principal.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -15,71 +15,45 @@
 ##
 from __future__ import print_function
 
-import os
+from urllib import quote
+from uuid import UUID
 
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.cred.credentials import UsernamePassword
-from twisted.internet.defer import inlineCallbacks
-from txdav.xml import element as davxml
-from txweb2.dav.fileop import rmdir
+
 from txweb2.dav.resource import AccessDeniedError
 from txweb2.http import HTTPError
 from txweb2.test.test_server import SimpleRequest
 
+from txdav.xml import element as davxml
+
+from twistedcaldav import carddavxml
 from twistedcaldav.cache import DisabledCacheNotifier
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.directory import augment, calendaruserproxy
-from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
-from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-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
-from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
-from twistedcaldav import carddavxml
-import twistedcaldav.test.util
+from twistedcaldav.directory.principal import (
+    DirectoryCalendarPrincipalResource,
+    DirectoryPrincipalResource,
+    DirectoryPrincipalTypeProvisioningResource,
+)
+from twistedcaldav.test.util import StoreTestCase
+from txdav.who.idirectory import AutoScheduleMode, RecordType as CalRecordType
+from twext.who.idirectory import RecordType
 
-from txdav.common.datastore.file import CommonDataStore
-from urllib import quote
 
-
-
-class ProvisionedPrincipals (twistedcaldav.test.util.TestCase):
+class ProvisionedPrincipals(StoreTestCase):
     """
     Directory service provisioned principals.
     """
+    @inlineCallbacks
     def setUp(self):
-        super(ProvisionedPrincipals, self).setUp()
+        yield super(ProvisionedPrincipals, self).setUp()
 
-        self.directoryServices = (
-            XMLDirectoryService(
-                {
-                    'xmlFile' : xmlFile,
-                    'augmentService' :
-                        augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-                }
-            ),
-        )
+        self.principalRootResource = self.actualRoot.getChild("principals")
 
-        # Set up a principals hierarchy for each service we're testing with
-        self.principalRootResources = {}
-        for directory in self.directoryServices:
-            name = directory.__class__.__name__
-            url = "/" + name + "/"
 
-            provisioningResource = DirectoryPrincipalProvisioningResource(url, directory)
-            directory.setPrincipalCollection(provisioningResource)
 
-            self.site.resource.putChild(name, provisioningResource)
-
-            self.principalRootResources[directory.__class__.__name__] = provisioningResource
-
-        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB(os.path.abspath(self.mktemp()))
-
-
     @inlineCallbacks
     def test_hierarchy(self):
         """
@@ -95,57 +69,109 @@
 
         DirectoryPrincipalResource.principalURL(),
         """
-        for directory in self.directoryServices:
-            #print("\n -> %s" % (directory.__class__.__name__,))
-            provisioningResource = self.principalRootResources[directory.__class__.__name__]
+        provisioningResource = self.principalRootResource
 
-            provisioningURL = "/" + directory.__class__.__name__ + "/"
-            self.assertEquals(provisioningURL, provisioningResource.principalCollectionURL())
+        provisioningURL = "/principals/"
+        self.assertEquals(
+            provisioningURL,
+            provisioningResource.principalCollectionURL()
+        )
 
-            principalCollections = provisioningResource.principalCollections()
-            self.assertEquals(set((provisioningURL,)), set(pc.principalCollectionURL() for pc in principalCollections))
+        principalCollections = provisioningResource.principalCollections()
+        self.assertEquals(
+            set((provisioningURL,)),
+            set(pc.principalCollectionURL() for pc in principalCollections)
+        )
 
-            recordTypes = set((yield provisioningResource.listChildren()))
-            self.assertEquals(recordTypes, set(directory.recordTypes()))
+        recordTypes = set((yield provisioningResource.listChildren()))
+        self.assertEquals(
+            recordTypes,
+            set(
+                [
+                    self.directory.recordTypeToOldName(rt) for rt in
+                    (
+                        self.directory.recordType.user,
+                        self.directory.recordType.group,
+                        self.directory.recordType.location,
+                        self.directory.recordType.resource,
+                        self.directory.recordType.address,
+                    )
+                ]
+            )
+        )
 
-            for recordType in recordTypes:
-                #print("   -> %s" % (recordType,))
-                typeResource = provisioningResource.getChild(recordType)
-                self.failUnless(isinstance(typeResource, DirectoryPrincipalTypeProvisioningResource))
+        for recordType in recordTypes:
+            typeResource = yield provisioningResource.getChild(recordType)
+            self.failUnless(
+                isinstance(
+                    typeResource,
+                    DirectoryPrincipalTypeProvisioningResource
+                )
+            )
 
-                typeURL = provisioningURL + recordType + "/"
-                self.assertEquals(typeURL, typeResource.principalCollectionURL())
+            typeURL = provisioningURL + recordType + "/"
+            self.assertEquals(
+                typeURL, typeResource.principalCollectionURL()
+            )
 
-                principalCollections = typeResource.principalCollections()
-                self.assertEquals(set((provisioningURL,)), set(pc.principalCollectionURL() for pc in principalCollections))
+            principalCollections = typeResource.principalCollections()
+            self.assertEquals(
+                set((provisioningURL,)),
+                set(
+                    pc.principalCollectionURL()
+                    for pc in principalCollections
+                )
+            )
 
-                shortNames = set((yield typeResource.listChildren()))
-                # Handle records with mulitple shortNames
-                expected = []
-                for r in directory.listRecords(recordType):
-                    if r.uid != "disabled":
-                        expected.extend(r.shortNames)
-                self.assertEquals(shortNames, set(expected))
+            shortNames = set((yield typeResource.listChildren()))
+            # Handle records with mulitple shortNames
+            expected = []
+            for r in (
+                yield self.directory.recordsWithRecordType(
+                    self.directory.oldNameToRecordType(recordType)
+                )
+            ):
+                expected.extend(r.shortNames)
+            self.assertEquals(shortNames, set(expected))
 
-                for shortName in shortNames:
-                    #print("     -> %s" % (shortName,))
-                    recordResource = typeResource.getChild(shortName)
-                    self.failUnless(isinstance(recordResource, DirectoryPrincipalResource))
+            for shortName in shortNames:
+                #print("     -> %s" % (shortName,))
+                recordResource = yield typeResource.getChild(shortName)
+                self.failUnless(
+                    isinstance(recordResource, DirectoryPrincipalResource)
+                )
 
-                    # shortName may be non-ascii
-                    recordURL = typeURL + quote(shortName) + "/"
-                    self.assertIn(recordURL, (recordResource.principalURL(),) + tuple(recordResource.alternateURIs()))
+                # shortName may be non-ascii
+                recordURL = typeURL + quote(shortName.encode("utf-8")) + "/"
+                self.assertIn(
+                    recordURL,
+                    (
+                        (recordResource.principalURL(),) +
+                        tuple(recordResource.alternateURIs())
+                    )
+                )
 
-                    principalCollections = recordResource.principalCollections()
-                    self.assertEquals(set((provisioningURL,)), set(pc.principalCollectionURL() for pc in principalCollections))
+                principalCollections = (
+                    recordResource.principalCollections()
+                )
+                self.assertEquals(
+                    set((provisioningURL,)),
+                    set(
+                        pc.principalCollectionURL()
+                        for pc in principalCollections
+                    )
+                )
 
 
+    @inlineCallbacks
     def test_allRecords(self):
         """
         Test of a test routine...
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if True:  # user.enabled:
                 self.assertEquals(recordResource.record, record)
 
 
@@ -153,85 +179,108 @@
     # DirectoryPrincipalProvisioningResource
     ##
 
+    @inlineCallbacks
     def test_principalForShortName(self):
         """
         DirectoryPrincipalProvisioningResource.principalForShortName()
         """
-        for provisioningResource, recordType, _ignore_recordResource, record in self._allRecords():
-            principal = provisioningResource.principalForShortName(recordType, record.shortNames[0])
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            principal = yield provisioningResource.principalForShortName(
+                recordType, record.shortNames[0]
+            )
+            if True:  # user.enabled:
                 self.failIf(principal is None)
                 self.assertEquals(record, principal.record)
             else:
                 self.failIf(principal is not None)
 
 
+    @inlineCallbacks
     def test_principalForUser(self):
         """
         DirectoryPrincipalProvisioningResource.principalForUser()
         """
-        for directory in self.directoryServices:
-            provisioningResource = self.principalRootResources[directory.__class__.__name__]
+        provisioningResource = self.principalRootResource
 
-            for user in directory.listRecords(DirectoryService.recordType_users):
-                userResource = provisioningResource.principalForUser(user.shortNames[0])
-                if user.enabled:
-                    self.failIf(userResource is None)
-                    self.assertEquals(user, userResource.record)
-                else:
-                    self.failIf(userResource is not None)
+        for user in (
+            yield self.directory.recordsWithRecordType(
+                self.directory.recordType.user
+            )
+        ):
+            userResource = yield provisioningResource.principalForUser(
+                user.shortNames[0]
+            )
+            if True:  # user.enabled:
+                self.failIf(userResource is None)
+                self.assertEquals(user, userResource.record)
+            else:
+                self.failIf(userResource is not None)
 
 
+    @inlineCallbacks
     def test_principalForAuthID(self):
         """
         DirectoryPrincipalProvisioningResource.principalForAuthID()
         """
-        for directory in self.directoryServices:
-            provisioningResource = self.principalRootResources[directory.__class__.__name__]
+        provisioningResource = self.principalRootResource
 
-            for user in directory.listRecords(DirectoryService.recordType_users):
-                creds = UsernamePassword(user.shortNames[0], "bogus")
-                userResource = provisioningResource.principalForAuthID(creds)
-                if user.enabled:
-                    self.failIf(userResource is None)
-                    self.assertEquals(user, userResource.record)
-                else:
-                    self.failIf(userResource is not None)
+        for user in (
+            yield self.directory.recordsWithRecordType(
+                self.directory.recordType.user
+            )
+        ):
+            creds = UsernamePassword(user.shortNames[0], "bogus")
+            userResource = yield provisioningResource.principalForAuthID(creds)
+            if True:  # user.enabled:
+                self.failIf(userResource is None)
+                self.assertEquals(user, userResource.record)
+            else:
+                self.failIf(userResource is not None)
 
 
+    @inlineCallbacks
     def test_principalForUID(self):
         """
         DirectoryPrincipalProvisioningResource.principalForUID()
         """
-        for provisioningResource, _ignore_recordType, _ignore_recordResource, record in self._allRecords():
-            principal = provisioningResource.principalForUID(record.uid)
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            principal = yield provisioningResource.principalForUID(record.uid)
+            if True:  # user.enabled:
                 self.failIf(principal is None)
                 self.assertEquals(record, principal.record)
             else:
                 self.failIf(principal is not None)
 
 
+    @inlineCallbacks
     def test_principalForRecord(self):
         """
         DirectoryPrincipalProvisioningResource.principalForRecord()
         """
-        for provisioningResource, _ignore_recordType, _ignore_recordResource, record in self._allRecords():
-            principal = provisioningResource.principalForRecord(record)
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            principal = yield provisioningResource.principalForRecord(record)
+            if True:  # user.enabled:
                 self.failIf(principal is None)
                 self.assertEquals(record, principal.record)
             else:
                 self.failIf(principal is not None)
 
 
+    @inlineCallbacks
     def test_principalForCalendarUserAddress(self):
         """
-        DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
+        DirectoryPrincipalProvisioningResource
+        .principalForCalendarUserAddress()
         """
         for (
-            provisioningResource, _ignore_recordType, recordResource, record
-        ) in self._allRecords():
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
 
             test_items = tuple(record.calendarUserAddresses)
             if recordResource:
@@ -243,62 +292,89 @@
                 test_items += (principalURL, alternateURL)
 
             for address in test_items:
-                principal = provisioningResource.principalForCalendarUserAddress(address)
-                if record.enabledForCalendaring:
+                principal = (
+                    yield provisioningResource
+                    .principalForCalendarUserAddress(address)
+                )
+                if record.hasCalendars:
                     self.failIf(principal is None)
                     self.assertEquals(record, principal.record)
                 else:
                     self.failIf(principal is not None)
 
         # Explicitly check the disabled record
-        provisioningResource = self.principalRootResources['XMLDirectoryService']
+        provisioningResource = yield self.actualRoot.getChild("principals")
 
         self.failUnlessIdentical(
-            provisioningResource.principalForCalendarUserAddress(
-                "mailto:nocalendar at example.com"
+            (
+                yield provisioningResource.principalForCalendarUserAddress(
+                    "mailto:nocalendar at example.com"
+                )
             ),
             None
         )
         self.failUnlessIdentical(
-            provisioningResource.principalForCalendarUserAddress(
-                "urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5"
+            (
+                yield provisioningResource.principalForCalendarUserAddress(
+                    "urn:uuid:543D28BA-F74F-4D5F-9243-B3E3A61171E5"
+                )
             ),
             None
         )
         self.failUnlessIdentical(
-            provisioningResource.principalForCalendarUserAddress(
-                "/principals/users/nocalendar/"
+            (
+                yield provisioningResource.principalForCalendarUserAddress(
+                    "/principals/users/nocalendar/"
+                )
             ),
             None
         )
         self.failUnlessIdentical(
-            provisioningResource.principalForCalendarUserAddress(
-                "/principals/__uids__/543D28BA-F74F-4D5F-9243-B3E3A61171E5/"
+            (
+                yield provisioningResource.principalForCalendarUserAddress(
+                    "/principals/__uids__/"
+                    "543D28BA-F74F-4D5F-9243-B3E3A61171E5/"
+                )
             ),
             None
         )
 
 
     @inlineCallbacks
-    def test_enabledForCalendaring(self):
+    def test_hasCalendars(self):
         """
-        DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
+        DirectoryPrincipalProvisioningResource
+        .principalForCalendarUserAddress()
         """
-        for provisioningResource, _ignore_recordType, _ignore_recordResource, record in self._allRecords():
-            principal = provisioningResource.principalForRecord(record)
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            principal = yield provisioningResource.principalForRecord(record)
+            if True:  # user.enabled:
                 self.failIf(principal is None)
             else:
                 self.failIf(principal is not None)
                 continue
-            if record.enabledForCalendaring:
-                self.assertTrue(isinstance(principal, DirectoryCalendarPrincipalResource))
+            if record.hasCalendars:
+                self.assertTrue(
+                    isinstance(principal, DirectoryCalendarPrincipalResource)
+                )
             else:
-                self.assertTrue(isinstance(principal, DirectoryPrincipalResource))
-                if record.enabledForAddressBooks:
-                    self.assertTrue(isinstance(principal, DirectoryCalendarPrincipalResource))
+                self.assertTrue(
+                    isinstance(principal, DirectoryPrincipalResource)
+                )
+                if record.hasContacts:
+                    self.assertTrue(
+                        isinstance(
+                            principal, DirectoryCalendarPrincipalResource
+                        )
+                    )
                 else:
-                    self.assertFalse(isinstance(principal, DirectoryCalendarPrincipalResource))
+                    self.assertFalse(
+                        isinstance(
+                            principal, DirectoryCalendarPrincipalResource
+                        )
+                    )
 
             @inlineCallbacks
             def hasProperty(property):
@@ -315,88 +391,150 @@
                 except:
                     self.fail("Wrong exception type")
                 else:
-                    self.fail("No exception principal: %s, property %s" % (principal, property,))
+                    self.fail(
+                        "No exception principal: %s, property %s"
+                        % (principal, property,)
+                    )
 
-            if record.enabledForCalendaring:
-                yield hasProperty((caldav_namespace, "calendar-home-set"))
-                yield hasProperty((caldav_namespace, "calendar-user-address-set"))
-                yield hasProperty((caldav_namespace, "schedule-inbox-URL"))
-                yield hasProperty((caldav_namespace, "schedule-outbox-URL"))
-                yield hasProperty((caldav_namespace, "calendar-user-type"))
-                yield hasProperty((calendarserver_namespace, "calendar-proxy-read-for"))
-                yield hasProperty((calendarserver_namespace, "calendar-proxy-write-for"))
-                yield hasProperty((calendarserver_namespace, "auto-schedule"))
+            if record.hasCalendars:
+                yield hasProperty(
+                    (caldav_namespace, "calendar-home-set")
+                )
+                yield hasProperty(
+                    (caldav_namespace, "calendar-user-address-set")
+                )
+                yield hasProperty(
+                    (caldav_namespace, "schedule-inbox-URL")
+                )
+                yield hasProperty(
+                    (caldav_namespace, "schedule-outbox-URL")
+                )
+                yield hasProperty(
+                    (caldav_namespace, "calendar-user-type")
+                )
+                yield hasProperty(
+                    (calendarserver_namespace, "calendar-proxy-read-for")
+                )
+                yield hasProperty(
+                    (calendarserver_namespace, "calendar-proxy-write-for")
+                )
+                # yield hasProperty(
+                #     (calendarserver_namespace, "auto-schedule")
+                # )
             else:
-                yield doesNotHaveProperty((caldav_namespace, "calendar-home-set"))
-                yield doesNotHaveProperty((caldav_namespace, "calendar-user-address-set"))
-                yield doesNotHaveProperty((caldav_namespace, "schedule-inbox-URL"))
-                yield doesNotHaveProperty((caldav_namespace, "schedule-outbox-URL"))
-                yield doesNotHaveProperty((caldav_namespace, "calendar-user-type"))
-                yield doesNotHaveProperty((calendarserver_namespace, "calendar-proxy-read-for"))
-                yield doesNotHaveProperty((calendarserver_namespace, "calendar-proxy-write-for"))
-                yield doesNotHaveProperty((calendarserver_namespace, "auto-schedule"))
+                yield doesNotHaveProperty(
+                    (caldav_namespace, "calendar-home-set")
+                )
+                yield doesNotHaveProperty(
+                    (caldav_namespace, "calendar-user-address-set")
+                )
+                yield doesNotHaveProperty(
+                    (caldav_namespace, "schedule-inbox-URL")
+                )
+                yield doesNotHaveProperty(
+                    (caldav_namespace, "schedule-outbox-URL")
+                )
+                yield doesNotHaveProperty(
+                    (caldav_namespace, "calendar-user-type")
+                )
+                yield doesNotHaveProperty(
+                    (calendarserver_namespace, "calendar-proxy-read-for")
+                )
+                yield doesNotHaveProperty(
+                    (calendarserver_namespace, "calendar-proxy-write-for")
+                )
+                # yield doesNotHaveProperty(
+                #     (calendarserver_namespace, "auto-schedule")
+                # )
 
-            if record.enabledForAddressBooks:
+            if record.hasContacts:
                 yield hasProperty(carddavxml.AddressBookHomeSet.qname())
             else:
-                yield doesNotHaveProperty(carddavxml.AddressBookHomeSet.qname())
+                yield doesNotHaveProperty(
+                    carddavxml.AddressBookHomeSet.qname()
+                )
 
 
+    @inlineCallbacks
     def test_enabledAsOrganizer(self):
         """
-        DirectoryPrincipalProvisioningResource.principalForCalendarUserAddress()
+        DirectoryPrincipalProvisioningResource
+        .principalForCalendarUserAddress()
         """
-
         ok_types = (
-            DirectoryService.recordType_users,
+            self.directory.recordType.user,
         )
-        for provisioningResource, recordType, _ignore_recordResource, record in self._allRecords():
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
 
-            if record.enabledForCalendaring:
-                principal = provisioningResource.principalForRecord(record)
+            if record.hasCalendars:
+                principal = (
+                    yield provisioningResource.principalForRecord(record)
+                )
                 self.failIf(principal is None)
-                self.assertEqual(principal.enabledAsOrganizer(), recordType in ok_types)
+                self.assertEqual(
+                    principal.enabledAsOrganizer(),
+                    recordType in ok_types
+                )
 
         config.Scheduling.Options.AllowGroupAsOrganizer = True
         config.Scheduling.Options.AllowLocationAsOrganizer = True
         config.Scheduling.Options.AllowResourceAsOrganizer = True
         ok_types = (
-            DirectoryService.recordType_users,
-            DirectoryService.recordType_groups,
-            DirectoryService.recordType_locations,
-            DirectoryService.recordType_resources,
+            self.directory.recordType.user,
+            self.directory.recordType.group,
+            self.directory.recordType.location,
+            self.directory.recordType.resource,
         )
-        for provisioningResource, recordType, _ignore_recordResource, record in self._allRecords():
-
-            if record.enabledForCalendaring:
-                principal = provisioningResource.principalForRecord(record)
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
+                principal = (
+                    yield provisioningResource.principalForRecord(record)
+                )
                 self.failIf(principal is None)
-                self.assertEqual(principal.enabledAsOrganizer(), recordType in ok_types)
+                self.assertEqual(
+                    principal.enabledAsOrganizer(),
+                    recordType in ok_types
+                )
 
 
-    # FIXME: Run DirectoryPrincipalProvisioningResource tests on DirectoryPrincipalTypeProvisioningResource also
+    # FIXME: Run DirectoryPrincipalProvisioningResource tests on
+    # DirectoryPrincipalTypeProvisioningResource also
 
     ##
     # DirectoryPrincipalResource
     ##
 
+    @inlineCallbacks
     def test_cacheNotifier(self):
         """
         Each DirectoryPrincipalResource should have a cacheNotifier attribute
         that is an instance of DisabledCacheNotifier
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
-                self.failUnless(isinstance(recordResource.cacheNotifier,
-                                           DisabledCacheNotifier))
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if True:  # user.enabled:
+                self.failUnless(
+                    isinstance(
+                        recordResource.cacheNotifier,
+                        DisabledCacheNotifier
+                    )
+                )
 
 
+    @inlineCallbacks
     def test_displayName(self):
         """
         DirectoryPrincipalResource.displayName()
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if True:  # user.enabled:
                 self.failUnless(recordResource.displayName())
 
 
@@ -405,10 +543,15 @@
         """
         DirectoryPrincipalResource.groupMembers()
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
-                members = yield recordResource.groupMembers()
-                self.failUnless(set(record.members()).issubset(set(r.record for r in members)))
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            members = yield recordResource.groupMembers()
+            self.failUnless(
+                set((yield record.members())).issubset(
+                    set(r.record for r in members)
+                )
+            )
 
 
     @inlineCallbacks
@@ -416,138 +559,157 @@
         """
         DirectoryPrincipalResource.groupMemberships()
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if True:  # user.enabled:
                 memberships = yield recordResource.groupMemberships()
-                self.failUnless(set(record.groups()).issubset(set(r.record for r in memberships if hasattr(r, "record"))))
+                self.failUnless(
+                    set((yield record.groups())).issubset(
+                        set(
+                            r.record
+                            for r in memberships if hasattr(r, "record")
+                        )
+                    )
+                )
 
 
+    @inlineCallbacks
     def test_principalUID(self):
         """
         DirectoryPrincipalResource.principalUID()
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
-                self.assertEquals(record.guid, recordResource.principalUID())
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            self.assertEquals(record.uid, recordResource.principalUID())
 
 
+    @inlineCallbacks
     def test_calendarUserAddresses(self):
         """
         DirectoryPrincipalResource.calendarUserAddresses()
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                self.assertEqual(set(record.calendarUserAddresses), set(recordResource.calendarUserAddresses()))
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
+                self.assertEqual(
+                    set(record.calendarUserAddresses),
+                    set(recordResource.calendarUserAddresses())
+                )
 
                 # Verify that if not enabled for calendaring, no CUAs:
-                record.enabledForCalendaring = False
+                record.hasCalendars = False
                 self.failIf(recordResource.calendarUserAddresses())
 
 
+    @inlineCallbacks
     def test_canonicalCalendarUserAddress(self):
         """
         DirectoryPrincipalResource.canonicalCalendarUserAddress()
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                self.failUnless(recordResource.canonicalCalendarUserAddress().startswith("urn:uuid:"))
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
+                if self.directory.fieldName.guid in record.fields:
+                    self.failUnless(
+                        recordResource.canonicalCalendarUserAddress()
+                        .startswith("urn:uuid:")
+                    )
+                elif self.directory.fieldName.emailAddresses in record.fields:
+                    self.failUnless(
+                        recordResource.canonicalCalendarUserAddress()
+                        .startswith("mailto:")
+                    )
+                else:
+                    self.failUnless(
+                        recordResource.canonicalCalendarUserAddress()
+                        .startswith("/principals/__uids__/")
+                    )
 
 
+    @inlineCallbacks
     def test_addressBookHomeURLs(self):
         """
         DirectoryPrincipalResource.addressBookHomeURLs(),
         """
-        # No addressbook home provisioner should result in no addressbook homes.
-        for provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabledForAddressBooks:
-                self.failIf(tuple(recordResource.addressBookHomeURLs()))
 
-        # Need to create a addressbook home provisioner for each service.
-        addressBookRootResources = {}
-
-        for directory in self.directoryServices:
-            path = os.path.join(self.docroot, directory.__class__.__name__)
-
-            if os.path.exists(path):
-                rmdir(path)
-            os.mkdir(path)
-
-            # Need a data store
-            _newStore = CommonDataStore(path, None, None, True, False)
-
-            provisioningResource = DirectoryAddressBookHomeProvisioningResource(
-                directory,
-                "/addressbooks/",
-                _newStore
-            )
-
-            addressBookRootResources[directory.__class__.__name__] = provisioningResource
-
-        # AddressBook home provisioners should result in addressBook homes.
-        for provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabledForAddressBooks:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasContacts:
                 homeURLs = tuple(recordResource.addressBookHomeURLs())
                 self.failUnless(homeURLs)
 
-                # Turn off enabledForAddressBooks and addressBookHomeURLs should
-                # be empty
-                record.enabledForAddressBooks = False
+                # Turn off hasContacts and addressBookHomeURLs
+                # should be empty
+                record.hasContacts = False
                 self.failIf(tuple(recordResource.addressBookHomeURLs()))
-                record.enabledForAddressBooks = True
+                record.hasContacts = True
 
-                addressBookRootURL = addressBookRootResources[record.service.__class__.__name__].url()
+                addressBookRootResource = yield self.actualRoot.getChild("addressbooks")
+                addressBookRootURL = addressBookRootResource.url()
 
                 for homeURL in homeURLs:
                     self.failUnless(homeURL.startswith(addressBookRootURL))
 
 
+    @inlineCallbacks
     def test_calendarHomeURLs(self):
         """
         DirectoryPrincipalResource.calendarHomeURLs(),
         DirectoryPrincipalResource.scheduleInboxURL(),
         DirectoryPrincipalResource.scheduleOutboxURL()
         """
-        # No calendar home provisioner should result in no calendar homes.
-        for provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                self.failIf(tuple(recordResource.calendarHomeURLs()))
-                self.failIf(recordResource.scheduleInboxURL())
-                self.failIf(recordResource.scheduleOutboxURL())
+        # # No calendar home provisioner should result in no calendar homes.
+        # for (
+        #     provisioningResource, recordType, recordResource, record
+        # ) in (yield self._allRecords()):
+        #     if record.hasCalendars:
+        #         self.failIf(tuple(recordResource.calendarHomeURLs()))
+        #         self.failIf(recordResource.scheduleInboxURL())
+        #         self.failIf(recordResource.scheduleOutboxURL())
 
-        # Need to create a calendar home provisioner for each service.
-        calendarRootResources = {}
+        # # Need to create a calendar home provisioner for each service.
+        # calendarRootResources = {}
 
-        for directory in self.directoryServices:
-            path = os.path.join(self.docroot, directory.__class__.__name__)
+        # path = os.path.join(self.docroot, self.directory.__class__.__name__)
 
-            if os.path.exists(path):
-                rmdir(path)
-            os.mkdir(path)
+        # if os.path.exists(path):
+        #     rmdir(path)
+        # os.mkdir(path)
 
-            # Need a data store
-            _newStore = CommonDataStore(path, None, None, True, False)
+        # # Need a data store
+        # _newStore = CommonDataStore(path, None, None, True, False)
 
-            provisioningResource = DirectoryCalendarHomeProvisioningResource(
-                directory,
-                "/calendars/",
-                _newStore
-            )
+        # provisioningResource = DirectoryCalendarHomeProvisioningResource(
+        #     self.directory,
+        #     "/calendars/",
+        #     _newStore
+        # )
 
-            calendarRootResources[directory.__class__.__name__] = provisioningResource
+        # calendarRootResources[self.directory.__class__.__name__] = (
+        #     provisioningResource
+        # )
 
         # Calendar home provisioners should result in calendar homes.
-        for provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
                 homeURLs = tuple(recordResource.calendarHomeURLs())
                 self.failUnless(homeURLs)
 
-                # Turn off enabledForCalendaring and calendarHomeURLs should
+                # Turn off hasCalendars and calendarHomeURLs should
                 # be empty
-                record.enabledForCalendaring = False
+                record.hasCalendars = False
                 self.failIf(tuple(recordResource.calendarHomeURLs()))
-                record.enabledForCalendaring = True
+                record.hasCalendars = True
 
-                calendarRootURL = calendarRootResources[record.service.__class__.__name__].url()
+                calendarRootResource = yield self.rootResource.getChild("calendars")
+                calendarRootURL = calendarRootResource.url()
 
                 inboxURL = recordResource.scheduleInboxURL()
                 outboxURL = recordResource.scheduleOutboxURL()
@@ -572,69 +734,121 @@
                 self.failIf(outboxURL)
 
 
+    @inlineCallbacks
     def test_canAutoSchedule(self):
         """
         DirectoryPrincipalResource.canAutoSchedule()
         """
 
-        # Set all resources and locations to auto-schedule, plus one user
-        for _ignore_provisioningResource, recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                if recordType in ("locations", "resources") or record.uid == "cdaboo":
-                    recordResource.record.autoSchedule = True
+        # This test used to set the autoschedule mode in a separate loop, but
+        # the records aren't cached, so I've moved this into the later loop
 
+        # # Set all resources and locations to auto-schedule, plus one user
+        # for (
+        #     provisioningResource, recordType, recordResource, record
+        # ) in (yield self._allRecords()):
+        #     if record.hasCalendars:
+        #         print("before", record, record.recordType, record.uid)
+        #         if (
+        #             recordType in (CalRecordType.location, CalRecordType.resource) or
+        #             record.uid == "5A985493-EE2C-4665-94CF-4DFEA3A89500"
+        #         ):
+        #             record.fields[record.service.fieldName.lookupByName("autoScheduleMode")] = AutoScheduleMode.acceptIfFreeDeclineIfBusy
+        #             print("modifying", record, record.fields)
+
         # Default state - resources and locations, enabled, others not
-        for _ignore_provisioningResource, recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                if recordType in ("locations", "resources"):
-                    self.assertTrue(recordResource.canAutoSchedule())
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
+                if recordType in (CalRecordType.location, CalRecordType.resource):
+                    self.assertTrue((yield recordResource.canAutoSchedule()))
                 else:
-                    self.assertFalse(recordResource.canAutoSchedule())
+                    self.assertFalse((yield recordResource.canAutoSchedule()))
 
         # Set config to allow users
         self.patch(config.Scheduling.Options.AutoSchedule, "AllowUsers", True)
-        for _ignore_provisioningResource, recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                if recordType in ("locations", "resources") or record.uid == "cdaboo":
-                    self.assertTrue(recordResource.canAutoSchedule())
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
+                if (
+                    recordType in (CalRecordType.location, CalRecordType.resource) or
+                    record.uid == u"5A985493-EE2C-4665-94CF-4DFEA3A89500"
+                ):
+                    record.fields[
+                        record.service.fieldName.lookupByName(
+                            "autoScheduleMode"
+                        )
+                    ] = AutoScheduleMode.acceptIfFreeDeclineIfBusy
+
+                    self.assertTrue((yield recordResource.canAutoSchedule()))
                 else:
-                    self.assertFalse(recordResource.canAutoSchedule())
+                    self.assertFalse((yield recordResource.canAutoSchedule()))
 
         # Set config to disallow all
         self.patch(config.Scheduling.Options.AutoSchedule, "Enabled", False)
-        for _ignore_provisioningResource, recordType, recordResource, record in self._allRecords():
-            if record.enabledForCalendaring:
-                self.assertFalse(recordResource.canAutoSchedule())
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if record.hasCalendars:
+                self.assertFalse((yield recordResource.canAutoSchedule()))
 
 
+    @inlineCallbacks
     def test_canAutoScheduleAutoAcceptGroup(self):
         """
         DirectoryPrincipalResource.canAutoSchedule(organizer)
         """
 
-        # Location "apollo" has an auto-accept group ("both_coasts") set in augments.xml,
-        # therefore any organizer in that group should be able to auto schedule
+        # Location "apollo" has an auto-accept group ("both_coasts") set in
+        # augments.xml, therefore any organizer in that group should be able to
+        # auto schedule
 
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.uid == "apollo":
+        record = yield self.directory.recordWithUID(u"apollo")
 
-                # No organizer
-                self.assertFalse(recordResource.canAutoSchedule())
+        # Turn off this record's autoschedule
+        record.fields[
+            record.service.fieldName.lookupByName(
+                "autoScheduleMode"
+            )
+        ] = AutoScheduleMode.none
 
-                # Organizer in auto-accept group
-                self.assertTrue(recordResource.canAutoSchedule(organizer="mailto:wsanchez at example.com"))
-                # Organizer not in auto-accept group
-                self.assertFalse(recordResource.canAutoSchedule(organizer="mailto:a at example.com"))
+        # No organizer
+        self.assertFalse((yield record.canAutoSchedule()))
 
+        # Organizer in auto-accept group
+        self.assertTrue(
+            (
+                yield record.canAutoSchedule(
+                    organizer="mailto:wsanchez at example.com"
+                )
+            )
+        )
+        # Organizer not in auto-accept group
+        self.assertFalse(
+            (
+                yield record.canAutoSchedule(
+                    organizer="mailto:a at example.com"
+                )
+            )
+        )
 
+
     @inlineCallbacks
     def test_defaultAccessControlList_principals(self):
         """
         Default access controls for principals.
         """
-        for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-            if record.enabled:
-                for args in _authReadOnlyPrivileges(self, recordResource, recordResource.principalURL()):
+        for (
+            provisioningResource, recordType, recordResource, record
+        ) in (yield self._allRecords()):
+            if True:  # user.enabled:
+                for args in (
+                    yield _authReadOnlyPrivileges(
+                        self, recordResource, recordResource.principalURL()
+                    )
+                ):
                     yield self._checkPrivileges(*args)
 
 
@@ -643,19 +857,26 @@
         """
         Default access controls for principal provisioning resources.
         """
-        for directory in self.directoryServices:
-            #print("\n -> %s" % (directory.__class__.__name__,))
-            provisioningResource = self.principalRootResources[directory.__class__.__name__]
+        provisioningResource = self.principalRootResource
 
-            for args in _authReadOnlyPrivileges(self, provisioningResource, provisioningResource.principalCollectionURL()):
-                yield self._checkPrivileges(*args)
+        for args in (
+            yield _authReadOnlyPrivileges(
+                self, provisioningResource,
+                provisioningResource.principalCollectionURL()
+            )
+        ):
+            yield self._checkPrivileges(*args)
 
-            for recordType in (yield provisioningResource.listChildren()):
-                #print("   -> %s" % (recordType,))
-                typeResource = provisioningResource.getChild(recordType)
+        for recordType in (yield provisioningResource.listChildren()):
+            #print("   -> %s" % (recordType,))
+            typeResource = yield provisioningResource.getChild(recordType)
 
-                for args in _authReadOnlyPrivileges(self, typeResource, typeResource.principalCollectionURL()):
-                    yield self._checkPrivileges(*args)
+            for args in (
+                yield _authReadOnlyPrivileges(
+                    self, typeResource, typeResource.principalCollectionURL()
+                )
+            ):
+                yield self._checkPrivileges(*args)
 
 
     def test_propertyToField(self):
@@ -668,23 +889,71 @@
             def qname(self):
                 return self.ns, self.name
 
-        provisioningResource = self.principalRootResources['XMLDirectoryService']
+        provisioningResource = self.principalRootResource
 
         expected = (
-            ("DAV:", "displayname", "morgen", "fullName", "morgen"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-type", "INDIVIDUAL", "recordType", "users"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-type", "GROUP", "recordType", "groups"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-type", "RESOURCE", "recordType", "resources"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-type", "ROOM", "recordType", "locations"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set", "/principals/__uids__/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA/", "guid", "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set", "http://example.com:8008/principals/__uids__/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA/", "guid", "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set", "urn:uuid:AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA", "guid", "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set", "/principals/users/example/", "recordName", "example"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set", "https://example.com:8443/principals/users/example/", "recordName", "example"),
-            ("urn:ietf:params:xml:ns:caldav", "calendar-user-address-set", "mailto:example at example.com", "emailAddresses", "example at example.com"),
-            ("http://calendarserver.org/ns/", "first-name", "morgen", "firstName", "morgen"),
-            ("http://calendarserver.org/ns/", "last-name", "sagen", "lastName", "sagen"),
-            ("http://calendarserver.org/ns/", "email-address-set", "example at example.com", "emailAddresses", "example at example.com"),
+            (
+                "DAV:", "displayname",
+                "morgen", "fullNames", "morgen"
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-type",
+                "INDIVIDUAL", "recordType", RecordType.user
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-type",
+                "GROUP", "recordType", RecordType.group
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-type",
+                "RESOURCE", "recordType", CalRecordType.resource
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-type",
+                "ROOM", "recordType", CalRecordType.location
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-address-set",
+                "/principals/__uids__/AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA/",
+                "uid", "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-address-set",
+                "http://example.com:8008/principals/__uids__/"
+                "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA/",
+                "uid", "AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA"
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-address-set",
+                "urn:uuid:AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA",
+                "guid", UUID("AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA")
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-address-set",
+                "/principals/users/example/", "recordName", "example"
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-address-set",
+                "https://example.com:8443/principals/users/example/",
+                "recordName", "example"
+            ),
+            (
+                "urn:ietf:params:xml:ns:caldav", "calendar-user-address-set",
+                "mailto:example at example.com",
+                "emailAddresses", "example at example.com"
+            ),
+            (
+                "http://calendarserver.org/ns/", "first-name",
+                "morgen", "firstName", "morgen"
+            ),
+            (
+                "http://calendarserver.org/ns/", "last-name",
+                "sagen", "lastName", "sagen"
+            ),
+            (
+                "http://calendarserver.org/ns/", "email-address-set",
+                "example at example.com", "emailAddresses", "example at example.com"
+            ),
         )
 
         for ns, property, match, field, converted in expected:
@@ -695,43 +964,57 @@
             )
 
 
+    @inlineCallbacks
     def _allRecords(self):
         """
         @return: an iterable of tuples
-            C{(provisioningResource, recordType, recordResource, record)}, where
-            C{provisioningResource} is the root provisioning resource,
-            C{recordType} is the record type,
-            C{recordResource} is the principal resource and
-            C{record} is the directory service record
-            for each record in each directory in C{directoryServices}.
+            C{(provisioningResource, recordType, recordResource, record)},
+            where C{provisioningResource} is the root provisioning resource,
+            C{recordType} is the record type, C{recordResource} is the
+            principal resource and C{record} is the directory service record
+            for each record the directory.
         """
-        for directory in self.directoryServices:
-            provisioningResource = self.principalRootResources[
-                directory.__class__.__name__
-            ]
-            for recordType in directory.recordTypes():
-                for record in directory.listRecords(recordType):
-                    recordResource = provisioningResource.principalForRecord(record)
-                    yield provisioningResource, recordType, recordResource, record
+        provisioningResource = self.principalRootResource
+        results = []
+        for recordType in self.directory.recordTypes():
+            for record in (
+                yield self.directory.recordsWithRecordType(recordType)
+            ):
+                recordResource = (
+                    yield provisioningResource.principalForRecord(record)
+                )
+                results.append(
+                    (provisioningResource, recordType, recordResource, record)
+                )
+        returnValue(results)
 
 
     def _checkPrivileges(self, resource, url, principal, privilege, allowed):
         request = SimpleRequest(self.site, "GET", "/")
 
         def gotResource(resource):
-            d = resource.checkPrivileges(request, (privilege,), principal=davxml.Principal(principal))
+            d = resource.checkPrivileges(
+                request, (privilege,), principal=davxml.Principal(principal)
+            )
             if allowed:
                 def onError(f):
                     f.trap(AccessDeniedError)
                     #print(resource.readDeadProperty(davxml.ACL))
-                    self.fail("%s should have %s privilege on %r" % (principal.sname(), privilege.sname(), resource))
+                    self.fail(
+                        "%s should have %s privilege on %r"
+                        % (principal.sname(), privilege.sname(), resource)
+                    )
                 d.addErrback(onError)
             else:
                 def expectAccessDenied(f):
                     f.trap(AccessDeniedError)
+
                 def onSuccess(_):
                     #print(resource.readDeadProperty(davxml.ACL))
-                    self.fail("%s should not have %s privilege on %r" % (principal.sname(), privilege.sname(), resource))
+                    self.fail(
+                        "%s should not have %s privilege on %r"
+                        % (principal.sname(), privilege.sname(), resource)
+                    )
                 d.addCallbacks(onSuccess, expectAccessDenied)
             return d
 
@@ -741,14 +1024,30 @@
 
 
 
+ at inlineCallbacks
 def _authReadOnlyPrivileges(self, resource, url):
     items = []
-    for _ignore_provisioningResource, _ignore_recordType, recordResource, record in self._allRecords():
-        if record.enabled:
-            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 (
+        provisioningResource, recordType, recordResource, record
+    ) in (yield self._allRecords()):
+        if True:  # user.enabled:
+            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
+    ))
 
+    results = []
     for principal, privilege, allowed in items:
-        yield resource, url, principal, privilege, allowed
+        results.append((resource, url, principal, privilege, allowed))
+
+    returnValue(results)

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_proxyprincipalmembers.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_proxyprincipalmembers.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,506 +0,0 @@
-##
-# Copyright (c) 2005-2014 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 DeferredList, inlineCallbacks, succeed
-from txdav.xml import element as davxml
-
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.test.util import xmlFile, augmentsFile, proxiesFile
-from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource, \
-    DirectoryPrincipalResource
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-
-import twistedcaldav.test.util
-from twistedcaldav.directory import augment, calendaruserproxy
-from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
-
-
-class ProxyPrincipals (twistedcaldav.test.util.TestCase):
-    """
-    Directory service provisioned principals.
-    """
-
-    @inlineCallbacks
-    def setUp(self):
-        super(ProxyPrincipals, self).setUp()
-
-        self.directoryFixture.addDirectoryService(XMLDirectoryService(
-            {
-                'xmlFile' : xmlFile,
-                'augmentService' :
-                    augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-            }
-        ))
-        calendaruserproxy.ProxyDBService = calendaruserproxy.ProxySqliteDB("proxies.sqlite")
-
-        # Set up a principals hierarchy for each service we're testing with
-        self.principalRootResources = {}
-        name = self.directoryService.__class__.__name__
-        url = "/" + name + "/"
-
-        provisioningResource = DirectoryPrincipalProvisioningResource(url, self.directoryService)
-
-        self.site.resource.putChild(name, provisioningResource)
-
-        self.principalRootResources[self.directoryService.__class__.__name__] = provisioningResource
-
-        yield XMLCalendarUserProxyLoader(proxiesFile.path).updateProxyDB()
-
-
-    def tearDown(self):
-        """ Empty the proxy db between tests """
-        return calendaruserproxy.ProxyDBService.clean() #@UndefinedVariable
-
-
-    def _getPrincipalByShortName(self, type, name):
-        provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
-        return provisioningResource.principalForShortName(type, name)
-
-
-    def _groupMembersTest(self, recordType, recordName, subPrincipalName, expectedMembers):
-        def gotMembers(members):
-            memberNames = set([p.displayName() for p in members])
-            self.assertEquals(memberNames, set(expectedMembers))
-
-        principal = self._getPrincipalByShortName(recordType, recordName)
-        if subPrincipalName is not None:
-            principal = principal.getChild(subPrincipalName)
-
-        d = principal.expandedGroupMembers()
-        d.addCallback(gotMembers)
-        return d
-
-
-    def _groupMembershipsTest(self, recordType, recordName, subPrincipalName, expectedMemberships):
-        def gotMemberships(memberships):
-            uids = set([p.principalUID() for p in memberships])
-            self.assertEquals(uids, set(expectedMemberships))
-
-        principal = self._getPrincipalByShortName(recordType, recordName)
-        if subPrincipalName is not None:
-            principal = principal.getChild(subPrincipalName)
-
-        d = principal.groupMemberships()
-        d.addCallback(gotMemberships)
-        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))
-        proxies = sorted([_principal.displayName() for _principal in proxies])
-        self.assertEquals(proxies, sorted(expectedProxies))
-
-
-    @inlineCallbacks
-    def test_multipleProxyAssignmentsAtOnce(self):
-        yield self._proxyForTest(
-            DirectoryService.recordType_users, "userb",
-            ('a',),
-            True
-        )
-        yield self._proxyForTest(
-            DirectoryService.recordType_users, "userc",
-            ('a',),
-            True
-        )
-
-
-    def test_groupMembersRegular(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        return self._groupMembersTest(
-            DirectoryService.recordType_groups, "both_coasts", None,
-            ("Chris Lecroy", "David Reid", "Wilfredo Sanchez", "West Coast", "East Coast", "Cyrus Daboo",),
-        )
-
-
-    def test_groupMembersRecursive(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        return self._groupMembersTest(
-            DirectoryService.recordType_groups, "recursive1_coasts", None,
-            ("Wilfredo Sanchez", "Recursive2 Coasts", "Cyrus Daboo",),
-        )
-
-
-    def test_groupMembersProxySingleUser(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        return self._groupMembersTest(
-            DirectoryService.recordType_locations, "gemini", "calendar-proxy-write",
-            ("Wilfredo Sanchez",),
-        )
-
-
-    def test_groupMembersProxySingleGroup(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        return self._groupMembersTest(
-            DirectoryService.recordType_locations, "mercury", "calendar-proxy-write",
-            ("Chris Lecroy", "David Reid", "Wilfredo Sanchez", "West Coast",),
-        )
-
-
-    def test_groupMembersProxySingleGroupWithNestedGroups(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        return self._groupMembersTest(
-            DirectoryService.recordType_locations, "apollo", "calendar-proxy-write",
-            ("Chris Lecroy", "David Reid", "Wilfredo Sanchez", "West Coast", "East Coast", "Cyrus Daboo", "Both Coasts",),
-        )
-
-
-    def test_groupMembersProxySingleGroupWithNestedRecursiveGroups(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        return self._groupMembersTest(
-            DirectoryService.recordType_locations, "orion", "calendar-proxy-write",
-            ("Wilfredo Sanchez", "Cyrus Daboo", "Recursive1 Coasts", "Recursive2 Coasts",),
-        )
-
-
-    def test_groupMembersProxySingleGroupWithNonCalendarGroup(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        ds = []
-
-        ds.append(self._groupMembersTest(
-            DirectoryService.recordType_resources, "non_calendar_proxy", "calendar-proxy-write",
-            ("Chris Lecroy", "Cyrus Daboo", "Non-calendar group"),
-        ))
-
-        ds.append(self._groupMembershipsTest(
-            DirectoryService.recordType_groups, "non_calendar_group", None,
-            ("non_calendar_proxy#calendar-proxy-write",),
-        ))
-
-        return DeferredList(ds)
-
-
-    def test_groupMembersProxyMissingUser(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        proxy = self._getPrincipalByShortName(DirectoryService.recordType_users, "cdaboo")
-        proxyGroup = proxy.getChild("calendar-proxy-write")
-
-        def gotMembers(members):
-            members.add("12345")
-            return proxyGroup._index().setGroupMembers("%s#calendar-proxy-write" % (proxy.principalUID(),), members)
-
-        def check(_):
-            return self._groupMembersTest(
-                DirectoryService.recordType_users, "cdaboo", "calendar-proxy-write",
-                (),
-            )
-
-        # Setup the fake entry in the DB
-        d = proxyGroup._index().getMembers("%s#calendar-proxy-write" % (proxy.principalUID(),))
-        d.addCallback(gotMembers)
-        d.addCallback(check)
-        return d
-
-
-    def test_groupMembershipsMissingUser(self):
-        """
-        DirectoryPrincipalResource.expandedGroupMembers()
-        """
-        # Setup the fake entry in the DB
-        fake_uid = "12345"
-        proxy = self._getPrincipalByShortName(DirectoryService.recordType_users, "cdaboo")
-        proxyGroup = proxy.getChild("calendar-proxy-write")
-
-        def gotMembers(members):
-            members.add("%s#calendar-proxy-write" % (proxy.principalUID(),))
-            return proxyGroup._index().setGroupMembers("%s#calendar-proxy-write" % (fake_uid,), members)
-
-        def check(_):
-            return self._groupMembershipsTest(
-                DirectoryService.recordType_users, "cdaboo", "calendar-proxy-write",
-                (),
-            )
-
-        d = proxyGroup._index().getMembers("%s#calendar-proxy-write" % (fake_uid,))
-        d.addCallback(gotMembers)
-        d.addCallback(check)
-        return d
-
-
-    @inlineCallbacks
-    def test_setGroupMemberSet(self):
-        class StubMemberDB(object):
-            def __init__(self):
-                self.members = set()
-
-            def setGroupMembers(self, uid, members):
-                self.members = members
-                return succeed(None)
-
-            def getMembers(self, uid):
-                return succeed(self.members)
-
-        user = self._getPrincipalByShortName(self.directoryService.recordType_users,
-                                           "cdaboo")
-
-        proxyGroup = user.getChild("calendar-proxy-write")
-
-        memberdb = StubMemberDB()
-
-        proxyGroup._index = (lambda: memberdb)
-
-        new_members = davxml.GroupMemberSet(
-            davxml.HRef.fromString(
-                "/XMLDirectoryService/__uids__/8B4288F6-CC82-491D-8EF9-642EF4F3E7D0/"),
-            davxml.HRef.fromString(
-                "/XMLDirectoryService/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/"))
-
-        yield proxyGroup.setGroupMemberSet(new_members, None)
-
-        self.assertEquals(
-            set([str(p) for p in memberdb.members]),
-            set(["5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1",
-                 "8B4288F6-CC82-491D-8EF9-642EF4F3E7D0"]))
-
-
-    @inlineCallbacks
-    def test_setGroupMemberSetNotifiesPrincipalCaches(self):
-        class StubCacheNotifier(object):
-            changedCount = 0
-            def changed(self):
-                self.changedCount += 1
-                return succeed(None)
-
-        user = self._getPrincipalByShortName(self.directoryService.recordType_users, "cdaboo")
-
-        proxyGroup = user.getChild("calendar-proxy-write")
-
-        notifier = StubCacheNotifier()
-
-        oldCacheNotifier = DirectoryPrincipalResource.cacheNotifierFactory
-
-        try:
-            DirectoryPrincipalResource.cacheNotifierFactory = (lambda _1, _2, **kwargs: notifier)
-
-            self.assertEquals(notifier.changedCount, 0)
-
-            yield proxyGroup.setGroupMemberSet(
-                davxml.GroupMemberSet(
-                    davxml.HRef.fromString(
-                        "/XMLDirectoryService/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/")),
-                None)
-
-            self.assertEquals(notifier.changedCount, 1)
-        finally:
-            DirectoryPrincipalResource.cacheNotifierFactory = oldCacheNotifier
-
-
-    def test_proxyFor(self):
-
-        return self._proxyForTest(
-            DirectoryService.recordType_users, "wsanchez",
-            ("Mercury Seven", "Gemini Twelve", "Apollo Eleven", "Orion",),
-            True
-        )
-
-
-    @inlineCallbacks
-    def test_proxyForDuplicates(self):
-
-        yield self._addProxy(
-            (DirectoryService.recordType_locations, "gemini",),
-            "calendar-proxy-write",
-            (DirectoryService.recordType_groups, "grunts",),
-        )
-
-        yield self._proxyForTest(
-            DirectoryService.recordType_users, "wsanchez",
-            ("Mercury Seven", "Gemini Twelve", "Apollo Eleven", "Orion",),
-            True
-        )
-
-
-    def test_readOnlyProxyFor(self):
-
-        return self._proxyForTest(
-            DirectoryService.recordType_users, "wsanchez",
-            ("Non-calendar proxy",),
-            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_NonAsciiProxy(self):
-        """
-        Ensure that principalURLs with non-ascii don't cause problems
-        within CalendarUserProxyPrincipalResource
-        """
-
-        recordType = DirectoryService.recordType_users
-        proxyType = "calendar-proxy-read"
-
-        record = self.directoryService.recordWithGUID("320B73A1-46E2-4180-9563-782DFDBE1F63")
-        provisioningResource = self.principalRootResources[self.directoryService.__class__.__name__]
-        principal = provisioningResource.principalForRecord(record)
-        proxyPrincipal = provisioningResource.principalForShortName(recordType,
-            "wsanchez")
-
-        yield self._addProxy(principal, proxyType, proxyPrincipal)
-        memberships = yield proxyPrincipal._calendar_user_proxy_index().getMemberships(proxyPrincipal.principalUID())
-        for uid in memberships:
-            provisioningResource.principalForUID(uid)
-
-
-    @inlineCallbacks
-    def test_getAllMembers(self):
-        """
-        getAllMembers( ) returns the unique set of guids that have been
-        delegated-to directly
-        """
-        self.assertEquals(
-            set((yield calendaruserproxy.ProxyDBService.getAllMembers())), #@UndefinedVariable
-            set([
-                u'00599DAF-3E75-42DD-9DB7-52617E79943F',
-                u'6423F94A-6B76-4A3A-815B-D52CFD77935D',
-                u'8A985493-EE2C-4665-94CF-4DFEA3A89500',
-                u'9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2',
-                u'both_coasts',
-                u'left_coast',
-                u'non_calendar_group',
-                u'recursive1_coasts',
-                u'recursive2_coasts',
-                u'EC465590-E9E9-4746-ACE8-6C756A49FE4D'])
-        )
-
-
-    @inlineCallbacks
-    def test_hideDisabledDelegates(self):
-        """
-        Delegates who are not enabledForLogin are "hidden" from the delegate lists
-        (but groups *are* allowed)
-        """
-
-        record = self.directoryService.recordWithGUID("EC465590-E9E9-4746-ACE8-6C756A49FE4D")
-
-        record.enabledForLogin = True
-        yield self._groupMembersTest(
-            DirectoryService.recordType_users, "delegator", "calendar-proxy-write",
-            ("Occasional Delegate", "Delegate Via Group", "Delegate Group"),
-        )
-
-        # Login disabled -- no longer shown as a delegate
-        record.enabledForLogin = False
-        yield self._groupMembersTest(
-            DirectoryService.recordType_users, "delegator", "calendar-proxy-write",
-            ("Delegate Via Group", "Delegate Group"),
-        )
-
-        # Login re-enabled -- once again a delegate (it wasn't not removed from proxydb)
-        record.enabledForLogin = True
-        yield self._groupMembersTest(
-            DirectoryService.recordType_users, "delegator", "calendar-proxy-write",
-            ("Occasional Delegate", "Delegate Via Group", "Delegate Group"),
-        )

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_resources.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_resources.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_resources.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,80 +0,0 @@
-##
-# Copyright (c) 2005-2014 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 twistedcaldav.config import config
-from twistedcaldav.test.util import TestCase
-from calendarserver.tools.util import getDirectory
-
-class ResourcesTestCase(TestCase):
-
-    def setUp(self):
-        super(ResourcesTestCase, self).setUp()
-
-        testRoot = os.path.join(".", os.path.dirname(__file__), "resources")
-
-        xmlFile = os.path.join(testRoot, "users-groups.xml")
-        config.DirectoryService.params.xmlFile = xmlFile
-
-        xmlFile = os.path.join(testRoot, "resources-locations.xml")
-        config.ResourceService.params.xmlFile = xmlFile
-        config.ResourceService.Enabled = True
-
-        xmlFile = os.path.join(testRoot, "augments.xml")
-        config.AugmentService.type = "twistedcaldav.directory.augment.AugmentXMLDB"
-        config.AugmentService.params.xmlFiles = (xmlFile,)
-
-# Uh, what's this testing?
-#    def test_loadConfig(self):
-#        directory = getDirectory()
-
-
-    def test_recordInPrimaryDirectory(self):
-        directory = getDirectory()
-
-        # Look up a user, which comes out of primary directory service
-        record = directory.recordWithUID("user01")
-        self.assertNotEquals(record, None)
-
-
-    def test_recordInSupplementalDirectory(self):
-        directory = getDirectory()
-
-        # Look up a resource, which comes out of locations/resources service
-        record = directory.recordWithUID("resource01")
-        self.assertNotEquals(record, None)
-
-
-    def test_augments(self):
-        directory = getDirectory()
-
-        # Primary directory
-        record = directory.recordWithUID("user01")
-        self.assertEquals(record.enabled, True)
-        self.assertEquals(record.enabledForCalendaring, True)
-        record = directory.recordWithUID("user02")
-        self.assertEquals(record.enabled, False)
-        self.assertEquals(record.enabledForCalendaring, False)
-
-        # Supplemental directory
-        record = directory.recordWithUID("resource01")
-        self.assertEquals(record.enabled, True)
-        self.assertEquals(record.enabledForCalendaring, True)
-        self.assertEquals(record.autoSchedule, True)
-        record = directory.recordWithUID("resource02")
-        self.assertEquals(record.enabled, False)
-        self.assertEquals(record.enabledForCalendaring, False)
-        self.assertEquals(record.autoSchedule, False)

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_wiki.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_wiki.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_wiki.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,116 +0,0 @@
-##
-# Copyright (c) 2005-2014 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.wiki import (
-    WikiDirectoryService, WikiDirectoryRecord, getWikiAccess
-)
-from twisted.internet.defer import inlineCallbacks, succeed
-from twisted.web.xmlrpc import Fault
-from txweb2.http import HTTPError
-from txweb2 import responsecode
-from twistedcaldav.config import config
-from twisted.web.error import Error as WebError
-
-class WikiTestCase(TestCase):
-    """
-    Test the Wiki Directory Service
-    """
-
-    def test_enabled(self):
-        service = WikiDirectoryService()
-        service.realmName = "Test"
-        record = WikiDirectoryRecord(service,
-            WikiDirectoryService.recordType_wikis,
-            "test",
-            None
-        )
-        self.assertTrue(record.enabled)
-        self.assertTrue(record.enabledForCalendaring)
-
-
-    @inlineCallbacks
-    def test_getWikiAccessLion(self):
-        """
-        XMLRPC Faults result in HTTPErrors
-        """
-        self.patch(config.Authentication.Wiki, "LionCompatibility", True)
-
-        def successful(self, user, wiki):
-            return succeed("read")
-
-        def fault2(self, user, wiki):
-            raise Fault(2, "Bad session")
-
-        def fault12(self, user, wiki):
-            raise Fault(12, "Non-existent wiki")
-
-        def fault13(self, user, wiki):
-            raise Fault(13, "Non-existent wiki")
-
-        access = (yield getWikiAccess("user", "wiki", method=successful))
-        self.assertEquals(access, "read")
-
-        for (method, code) in (
-            (fault2, responsecode.FORBIDDEN),
-            (fault12, responsecode.NOT_FOUND),
-            (fault13, responsecode.SERVICE_UNAVAILABLE),
-        ):
-            try:
-                access = (yield getWikiAccess("user", "wiki", method=method))
-            except HTTPError, e:
-                self.assertEquals(e.response.code, code)
-            except:
-                self.fail("Incorrect exception")
-            else:
-                self.fail("Didn't raise exception")
-
-
-    @inlineCallbacks
-    def test_getWikiAccess(self):
-        """
-        Non-200 GET responses result in HTTPErrors
-        """
-        self.patch(config.Authentication.Wiki, "LionCompatibility", False)
-
-        def successful(user, wiki, host=None, port=None):
-            return succeed("read")
-
-        def forbidden(user, wiki, host=None, port=None):
-            raise WebError(403, message="Unknown user")
-
-        def notFound(user, wiki, host=None, port=None):
-            raise WebError(404, message="Non-existent wiki")
-
-        def other(user, wiki, host=None, port=None):
-            raise WebError(500, "Error")
-
-        access = (yield getWikiAccess("user", "wiki", method=successful))
-        self.assertEquals(access, "read")
-
-        for (method, code) in (
-            (forbidden, responsecode.FORBIDDEN),
-            (notFound, responsecode.NOT_FOUND),
-            (other, responsecode.SERVICE_UNAVAILABLE),
-        ):
-            try:
-                access = (yield getWikiAccess("user", "wiki", method=method))
-            except HTTPError, e:
-                self.assertEquals(e.response.code, code)
-            except:
-                self.fail("Incorrect exception")
-            else:
-                self.fail("Didn't raise exception")

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_xmlfile.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_xmlfile.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/test_xmlfile.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,375 +0,0 @@
-##
-# Copyright (c) 2005-2014 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 twext.python.filepath import CachingFilePath as FilePath
-
-from twistedcaldav.directory import augment
-from twistedcaldav.directory.directory import DirectoryService
-import twistedcaldav.directory.test.util
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-from twistedcaldav.test.util import TestCase, xmlFile, augmentsFile
-
-# FIXME: Add tests for GUID hooey, once we figure out what that means here
-
-class XMLFileBase(object):
-    """
-    L{XMLFileBase} is a base/mix-in object for testing L{XMLDirectoryService}
-    (or things that depend on L{IDirectoryService} and need a simple
-    implementation to use).
-    """
-    recordTypes = set((
-        DirectoryService.recordType_users,
-        DirectoryService.recordType_groups,
-        DirectoryService.recordType_locations,
-        DirectoryService.recordType_resources,
-        DirectoryService.recordType_addresses,
-    ))
-
-    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",)   },
-        "nocalendar" : {"password": "radnelacon", "guid": "543D28BA-F74F-4D5F-9243-B3E3A61171E5", "addresses": ()},
-        "user01"     : {"password": "01user", "guid": None, "addresses": ("mailto:c4ca4238a0 at example.com",)},
-        "user02"     : {"password": "02user", "guid": None, "addresses": ("mailto:c81e728d9d at example.com",)},
-   }
-
-    groups = {
-        "admin"      : {"password": "admin", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_groups, "managers"),)},
-        "managers"   : {"password": "managers", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users , "lecroy"),)},
-        "grunts"     : {"password": "grunts", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users , "wsanchez"),
-                                                                                               (DirectoryService.recordType_users , "cdaboo"),
-                                                                                               (DirectoryService.recordType_users , "dreid"))},
-        "right_coast": {"password": "right_coast", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users , "cdaboo"),)},
-        "left_coast" : {"password": "left_coast", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users , "wsanchez"),
-                                                                                               (DirectoryService.recordType_users , "dreid"),
-                                                                                               (DirectoryService.recordType_users , "lecroy"))},
-        "both_coasts": {"password": "both_coasts", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_groups, "right_coast"),
-                                                                                               (DirectoryService.recordType_groups, "left_coast"))},
-        "recursive1_coasts": {"password": "recursive1_coasts", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_groups, "recursive2_coasts"),
-                                                                                               (DirectoryService.recordType_users, "wsanchez"))},
-        "recursive2_coasts": {"password": "recursive2_coasts", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_groups, "recursive1_coasts"),
-                                                                                               (DirectoryService.recordType_users, "cdaboo"))},
-        "non_calendar_group": {"password": "non_calendar_group", "guid": None, "addresses": (), "members": ((DirectoryService.recordType_users , "cdaboo"),
-                                                                                               (DirectoryService.recordType_users , "lecroy"))},
-   }
-
-    locations = {
-        "mercury": {"password": "mercury", "guid": None, "addresses": ("mailto:mercury at example.com",)},
-        "gemini" : {"password": "gemini", "guid": None, "addresses": ("mailto:gemini at example.com",)},
-        "apollo" : {"password": "apollo", "guid": None, "addresses": ("mailto:apollo at example.com",)},
-        "orion"  : {"password": "orion", "guid": None, "addresses": ("mailto:orion at example.com",)},
-   }
-
-    resources = {
-        "transporter"        : {"password": "transporter", "guid": None, "addresses": ("mailto:transporter at example.com",)       },
-        "ftlcpu"             : {"password": "ftlcpu", "guid": None, "addresses": ("mailto:ftlcpu at example.com",)            },
-        "non_calendar_proxy" : {"password": "non_calendar_proxy", "guid": "non_calendar_proxy", "addresses": ("mailto:non_calendar_proxy at example.com",)},
-   }
-
-
-    def xmlFile(self):
-        """
-        Create a L{FilePath} that points to a temporary file containing a copy
-        of C{twistedcaldav/directory/test/accounts.xml}.
-
-        @see: L{xmlFile}
-
-        @rtype: L{FilePath}
-        """
-        if not hasattr(self, "_xmlFile"):
-            self._xmlFile = FilePath(self.mktemp())
-            xmlFile.copyTo(self._xmlFile)
-        return self._xmlFile
-
-
-    def augmentsFile(self):
-        """
-        Create a L{FilePath} that points to a temporary file containing a copy
-        of C{twistedcaldav/directory/test/augments.xml}.
-
-        @see: L{augmentsFile}
-
-        @rtype: L{FilePath}
-        """
-        if not hasattr(self, "_augmentsFile"):
-            self._augmentsFile = FilePath(self.mktemp())
-            augmentsFile.copyTo(self._augmentsFile)
-        return self._augmentsFile
-
-
-    def service(self):
-        """
-        Create an L{XMLDirectoryService} based on the contents of the paths
-        returned by L{XMLFileBase.augmentsFile} and L{XMLFileBase.xmlFile}.
-
-        @rtype: L{XMLDirectoryService}
-        """
-        return XMLDirectoryService(
-            {
-                'xmlFile': self.xmlFile(),
-                'augmentService':
-                    augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)),
-            },
-            alwaysStat=True
-        )
-
-
-
-class XMLFile (
-    XMLFileBase,
-    twistedcaldav.directory.test.util.BasicTestCase,
-    twistedcaldav.directory.test.util.DigestTestCase
-):
-    """
-    Test XML file based directory implementation.
-    """
-
-    def test_changedXML(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>admin</uid>
-    <guid>admin</guid>
-    <password>nimda</password>
-    <name>Super User</name>
-  </user>
-</accounts>
-"""
-        )
-        for recordType, expectedRecords in (
-            (DirectoryService.recordType_users     , ("admin",)),
-            (DirectoryService.recordType_groups    , ()),
-            (DirectoryService.recordType_locations , ()),
-            (DirectoryService.recordType_resources , ()),
-        ):
-            # Fault records in
-            for name in expectedRecords:
-                service.recordWithShortName(recordType, name)
-
-            self.assertEquals(
-                set(r.shortNames[0] for r in service.listRecords(recordType)),
-                set(expectedRecords)
-            )
-
-
-    def test_okAutoSchedule(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>
-    <guid>myoffice</guid>
-    <password>nimda</password>
-    <name>Super User</name>
-  </location>
-</accounts>
-"""
-        )
-        self.augmentsFile().open("w").write(
-"""<?xml version="1.0" encoding="utf-8"?>
-<!DOCTYPE accounts SYSTEM "accounts.dtd">
-<augments>
-  <record>
-    <uid>myoffice</uid>
-    <enable>true</enable>
-    <enable-calendar>true</enable-calendar>
-    <auto-schedule>true</auto-schedule>
-  </record>
-</augments>
-"""
-        )
-        service.augmentService.refresh()
-
-        for recordType, expectedRecords in (
-            (DirectoryService.recordType_users     , ()),
-            (DirectoryService.recordType_groups    , ()),
-            (DirectoryService.recordType_locations , ("my office",)),
-            (DirectoryService.recordType_resources , ()),
-        ):
-            # Fault records in
-            for name in expectedRecords:
-                service.recordWithShortName(recordType, name)
-
-            self.assertEquals(
-                set(r.shortNames[0] for r in service.listRecords(recordType)),
-                set(expectedRecords)
-            )
-        self.assertTrue(service.recordWithShortName(DirectoryService.recordType_locations, "my office").autoSchedule)
-
-
-    def test_okDisableCalendar(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">
-  <group>
-    <uid>enabled</uid>
-    <guid>enabled</guid>
-    <password>enabled</password>
-    <name>Enabled</name>
-  </group>
-  <group>
-    <uid>disabled</uid>
-    <guid>disabled</guid>
-    <password>disabled</password>
-    <name>Disabled</name>
-  </group>
-</accounts>
-"""
-        )
-
-        for recordType, expectedRecords in (
-            (DirectoryService.recordType_users     , ()),
-            (DirectoryService.recordType_groups    , ("enabled", "disabled")),
-            (DirectoryService.recordType_locations , ()),
-            (DirectoryService.recordType_resources , ()),
-        ):
-            # Fault records in
-            for name in expectedRecords:
-                service.recordWithShortName(recordType, name)
-
-            self.assertEquals(
-                set(r.shortNames[0] for r in service.listRecords(recordType)),
-                set(expectedRecords)
-            )
-
-        # All groups are disabled
-        self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "enabled").enabledForCalendaring)
-        self.assertFalse(service.recordWithShortName(DirectoryService.recordType_groups, "disabled").enabledForCalendaring)
-
-
-    def test_readExtras(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>
-    <guid>myoffice</guid>
-    <name>My Office</name>
-    <extras>
-        <comment>This is the comment</comment>
-        <capacity>40</capacity>
-    </extras>
-  </location>
-</accounts>
-"""
-        )
-
-        record = service.recordWithShortName(
-            DirectoryService.recordType_locations, "my office")
-        self.assertEquals(record.guid, "myoffice")
-        self.assertEquals(record.extras["comment"], "This is the comment")
-        self.assertEquals(record.extras["capacity"], "40")
-
-
-    def test_writeExtras(self):
-        service = self.service()
-
-        service.createRecord(DirectoryService.recordType_locations, "newguid",
-            shortNames=("New office",),
-            fullName="My New Office",
-            address="1 Infinite Loop, Cupertino, CA",
-            capacity="10",
-            comment="Test comment",
-        )
-
-        record = service.recordWithShortName(
-            DirectoryService.recordType_locations, "New office")
-        self.assertEquals(record.extras["comment"], "Test comment")
-        self.assertEquals(record.extras["capacity"], "10")
-
-        service.updateRecord(DirectoryService.recordType_locations, "newguid",
-            shortNames=("New office",),
-            fullName="My Newer Office",
-            address="2 Infinite Loop, Cupertino, CA",
-            capacity="20",
-            comment="Test comment updated",
-        )
-
-        record = service.recordWithShortName(
-            DirectoryService.recordType_locations, "New office")
-        self.assertEquals(record.fullName, "My Newer Office")
-        self.assertEquals(record.extras["address"], "2 Infinite Loop, Cupertino, CA")
-        self.assertEquals(record.extras["comment"], "Test comment updated")
-        self.assertEquals(record.extras["capacity"], "20")
-
-        service.destroyRecord(DirectoryService.recordType_locations, "newguid")
-
-        record = service.recordWithShortName(
-            DirectoryService.recordType_locations, "New office")
-        self.assertEquals(record, None)
-
-
-    def test_indexing(self):
-        service = self.service()
-        self.assertNotEquals(None, service._lookupInIndex(service.recordType_users, service.INDEX_TYPE_SHORTNAME, "usera"))
-        self.assertNotEquals(None, service._lookupInIndex(service.recordType_users, service.INDEX_TYPE_CUA, "mailto:wsanchez at example.com"))
-        self.assertNotEquals(None, service._lookupInIndex(service.recordType_users, service.INDEX_TYPE_GUID, "9FF60DAD-0BDE-4508-8C77-15F0CA5C8DD2"))
-        self.assertNotEquals(None, service._lookupInIndex(service.recordType_locations, service.INDEX_TYPE_SHORTNAME, "orion"))
-        self.assertEquals(None, service._lookupInIndex(service.recordType_users, service.INDEX_TYPE_CUA, "mailto:nobody at example.com"))
-
-
-    def test_repeat(self):
-        service = self.service()
-        record = service.recordWithShortName(
-            DirectoryService.recordType_users, "user01")
-        self.assertEquals(record.fullName, "c4ca4238a0b923820dcc509a6f75849bc4c User 01")
-        self.assertEquals(record.firstName, "c4ca4")
-        self.assertEquals(record.lastName, "c4ca4238a User 01")
-        self.assertEquals(record.emailAddresses, set(['c4ca4238a0 at example.com']))
-
-
-
-class XMLFileSubset (XMLFileBase, TestCase):
-    """
-    Test the recordTypes subset feature of XMLFile service.
-    """
-    recordTypes = set((
-        DirectoryService.recordType_users,
-        DirectoryService.recordType_groups,
-    ))
-
-
-    def test_recordTypesSubset(self):
-        directory = XMLDirectoryService(
-            {
-                'xmlFile' : self.xmlFile(),
-                'augmentService' :
-                    augment.AugmentXMLDB(xmlFiles=(self.augmentsFile().path,)),
-                'recordTypes' :
-                    (
-                        DirectoryService.recordType_users,
-                        DirectoryService.recordType_groups
-                    ),
-            },
-            alwaysStat=True
-        )
-        self.assertEquals(set(("users", "groups")), set(directory.recordTypes()))

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/test/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -212,9 +212,9 @@
             guid = record.guid
 
         addresses = set(value("addresses"))
-        if record.enabledForCalendaring:
+        if record.hasCalendars:
             addresses.add("urn:uuid:%s" % (record.guid,))
-            addresses.add("/principals/__uids__/%s/" % (record.guid,))
+            addresses.add("/principals/__uids__/%s/" % (record.uid,))
             addresses.add("/principals/%s/%s/" % (record.recordType, record.shortNames[0],))
 
         if hasattr(record.service, "recordTypePrefix"):

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -34,7 +34,10 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 from txdav.xml import element as davxml
 from uuid import UUID, uuid5
+from twisted.python.failure import Failure
+from twisted.web.template import tags
 
+
 log = Logger()
 
 def uuidFromName(namespace, name):
@@ -148,3 +151,76 @@
         else:
             response = StatusResponse(responsecode.NOT_FOUND, "Resource not found")
             returnValue(response)
+
+
+
+
+def formatLink(url):
+    """
+    Convert a URL string into some twisted.web.template DOM objects for
+    rendering as a link to itself.
+    """
+    return tags.a(href=url)(url)
+
+
+
+def formatLinks(urls):
+    """
+    Format a list of URL strings as a list of twisted.web.template DOM links.
+    """
+    return formatList(formatLink(link) for link in urls)
+
+
+def formatPrincipals(principals):
+    """
+    Format a list of principals into some twisted.web.template DOM objects.
+    """
+    def recordKey(principal):
+        try:
+            record = principal.record
+        except AttributeError:
+            try:
+                record = principal.parent.record
+            except:
+                return None
+        return (record.recordType, record.shortNames[0])
+
+
+    def describe(principal):
+        if hasattr(principal, "record"):
+            return " - %s" % (principal.record.displayName,)
+        else:
+            return ""
+
+    return formatList(
+        tags.a(href=principal.principalURL())(
+            str(principal), describe(principal)
+        )
+        for principal in sorted(principals, key=recordKey)
+    )
+
+
+
+def formatList(iterable):
+    """
+    Format a list of stuff as an interable.
+    """
+    thereAreAny = False
+    try:
+        item = None
+        for item in iterable:
+            thereAreAny = True
+            yield " -> "
+            if item is None:
+                yield "None"
+            else:
+                yield item
+            yield "\n"
+    except Exception, e:
+        log.error("Exception while rendering: %s" % (e,))
+        Failure().printTraceback()
+        yield "  ** %s **: %s\n" % (e.__class__.__name__, e)
+    if not thereAreAny:
+        yield " '()\n"
+
+

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/wiki.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/wiki.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/wiki.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,367 +0,0 @@
-##
-# Copyright (c) 2006-2014 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.
-##
-
-"""
-Directory service implementation for users who are allowed to authorize
-as other principals.
-"""
-
-__all__ = [
-    "WikiDirectoryService",
-]
-
-from calendarserver.platform.darwin.wiki import accessForUserToWiki
-
-from twext.internet.gaiendpoint import MultiFailure
-from twext.python.log import Logger
-from txweb2 import responsecode
-from txweb2.auth.wrapper import UnauthorizedResponse
-from txweb2.dav.resource import TwistedACLInheritable
-from txweb2.http import HTTPError, StatusResponse
-
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twisted.web.error import Error as WebError
-from twisted.web.xmlrpc import Proxy, Fault
-
-from twistedcaldav.config import config
-from twistedcaldav.directory.directory import DirectoryService, \
-    DirectoryRecord, UnknownRecordTypeError
-
-from txdav.xml import element as davxml
-
-log = Logger()
-
-class WikiDirectoryService(DirectoryService):
-    """
-    L{IDirectoryService} implementation for Wikis.
-    """
-    baseGUID = "D79EF1E0-9A42-11DD-AD8B-0800200C9A66"
-
-    realmName = None
-
-    recordType_wikis = "wikis"
-
-    UIDPrefix = "wiki-"
-
-
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.realmName)
-
-
-    def __init__(self):
-        super(WikiDirectoryService, self).__init__()
-        self.byUID = {}
-        self.byShortName = {}
-
-
-    def recordTypes(self):
-        return (WikiDirectoryService.recordType_wikis,)
-
-
-    def listRecords(self, recordType):
-        return ()
-
-
-    def recordWithShortName(self, recordType, shortName):
-        if recordType != WikiDirectoryService.recordType_wikis:
-            raise UnknownRecordTypeError(recordType)
-
-        if shortName in self.byShortName:
-            record = self.byShortName[shortName]
-            return record
-
-        record = self._addRecord(shortName)
-        return record
-
-
-    def recordWithUID(self, uid):
-
-        if uid in self.byUID:
-            record = self.byUID[uid]
-            return record
-
-        if uid.startswith(self.UIDPrefix):
-            shortName = uid[len(self.UIDPrefix):]
-            record = self._addRecord(shortName)
-            return record
-        else:
-            return None
-
-
-    def _addRecord(self, shortName):
-
-        record = WikiDirectoryRecord(
-            self,
-            WikiDirectoryService.recordType_wikis,
-            shortName,
-            None
-        )
-        self.byUID[record.uid] = record
-        self.byShortName[shortName] = record
-        return record
-
-
-
-class WikiDirectoryRecord(DirectoryRecord):
-    """
-    L{DirectoryRecord} implementation for Wikis.
-    """
-
-    def __init__(self, service, recordType, shortName, entry):
-        super(WikiDirectoryRecord, self).__init__(
-            service=service,
-            recordType=recordType,
-            guid=None,
-            shortNames=(shortName,),
-            fullName=shortName,
-            enabledForCalendaring=True,
-            uid="%s%s" % (WikiDirectoryService.UIDPrefix, shortName),
-        )
-        # Wiki enabling doesn't come from augments db, so enable here...
-        self.enabled = True
-
-
-
- at inlineCallbacks
-def getWikiAccess(userID, wikiID, method=None):
-    """
-    Ask the wiki server we're paired with what level of access the userID has
-    for the given wikiID.  Possible values are "read", "write", and "admin"
-    (which we treat as "write").
-
-    @param userID: the GUID (UUID) of the user's directory record.
-    @type userID: L{bytes} (UTF-8)
-
-    @param wikiID: the short name of the wiki principal's synthetic directory
-        record.  (See L{WikiDirectoryService}).
-    @type wikiID: L{bytes} (UTF-8)
-
-    @return: A string indicating the level of access that the given user has to
-        the given wiki.  Possible values are:
-
-        1. C{b"no-access"} for read-only access
-
-        2. C{b"no-access"} for read/write access
-
-        3. C{b"no-access"} for administrative access (which, for calendaring
-           purposes, should be equialent to read/write)
-
-        4. C{b"no-access"} for a user who is not allowed to see the wiki at
-           all.
-
-    @rtype: L{bytes}
-
-    @raise: L{HTTPError} indicating that there is a problem requesting
-        permission information.  This may be raised with a few different status
-        codes, each indicating a different problem:
-
-        1. L{responsecode.FORBIDDEN}: The user represented by C{userID} did not
-           exist.
-
-        2. L{responsecode.NOT_FOUND}: The wiki represented by C{wikiID} did not
-           exist.
-
-        3. L{responsecode.SERVICE_UNAVAILABLE}: The service that we are
-           checking permissions with is currently offline or responding with an
-           unknown fault.
-    """
-    wikiConfig = config.Authentication.Wiki
-    if method is None:
-        if wikiConfig.LionCompatibility:
-            method = Proxy(wikiConfig["URL"]).callRemote
-        else:
-            method = accessForUserToWiki
-    try:
-
-        log.debug("Looking up Wiki ACL for: user [%s], wiki [%s]" % (userID,
-            wikiID))
-        if wikiConfig.LionCompatibility:
-            access = (yield method(wikiConfig["WikiMethod"],
-                userID, wikiID))
-        else:
-            access = (yield method(userID, wikiID,
-                host=wikiConfig.CollabHost, port=wikiConfig.CollabPort))
-
-        log.debug("Wiki ACL result: user [%s], wiki [%s], access [%s]" %
-            (userID, wikiID, access))
-        returnValue(access)
-
-    except MultiFailure, e:
-        log.error("Wiki ACL error: user [%s], wiki [%s], MultiFailure [%s]" %
-            (userID, wikiID, e))
-        raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE,
-            "\n".join([str(f) for f in e.failures])))
-
-    except Fault, fault:
-
-        log.debug("Wiki ACL result: user [%s], wiki [%s], FAULT [%s]" % (userID,
-            wikiID, fault))
-
-        if fault.faultCode == 2: # non-existent user
-            raise HTTPError(StatusResponse(responsecode.FORBIDDEN,
-                fault.faultString))
-
-        elif fault.faultCode == 12: # non-existent wiki
-            raise HTTPError(StatusResponse(responsecode.NOT_FOUND,
-                fault.faultString))
-
-        else:
-            # Unknown fault returned from wiki server.  Log the error and
-            # return 503 Service Unavailable to the client.
-            log.error("Wiki ACL error: user [%s], wiki [%s], FAULT [%s]" %
-                (userID, wikiID, fault))
-            raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE,
-                fault.faultString))
-
-    except WebError, w:
-        status = int(w.status)
-
-        log.debug("Wiki ACL result: user [%s], wiki [%s], status [%s]" %
-            (userID, wikiID, status))
-
-        if status == responsecode.FORBIDDEN: # non-existent user
-            raise HTTPError(StatusResponse(responsecode.FORBIDDEN,
-                "Unknown User"))
-
-        elif status == responsecode.NOT_FOUND: # non-existent wiki
-            raise HTTPError(StatusResponse(responsecode.NOT_FOUND,
-                "Unknown Wiki"))
-
-        else:
-            # Unknown fault returned from wiki server.  Log the error and
-            # return 503 Service Unavailable to the client.
-            log.error("Wiki ACL error: user [%s], wiki [%s], status [%s]" %
-                (userID, wikiID, status))
-            raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE,
-                w.message))
-
-
-
- at inlineCallbacks
-def getWikiACL(resource, request):
-    """
-    Ask the wiki server we're paired with what level of access the authnUser has.
-
-    Returns an ACL.
-
-    Wiki authentication is a bit tricky because the end-user accessing a group
-    calendar may not actually be enabled for calendaring.  Therefore in that
-    situation, the authzUser will have been replaced with the wiki principal
-    in locateChild( ), so that any changes the user makes will have the wiki
-    as the originator.  The authnUser will always be the end-user.
-    """
-    from twistedcaldav.directory.principal import DirectoryPrincipalResource
-
-    if (not hasattr(resource, "record") or
-        resource.record.recordType != WikiDirectoryService.recordType_wikis):
-        returnValue(None)
-
-    if hasattr(request, 'wikiACL'):
-        returnValue(request.wikiACL)
-
-    userID = "unauthenticated"
-    wikiID = resource.record.shortNames[0]
-
-    try:
-        url = str(request.authnUser.children[0])
-        principal = (yield request.locateResource(url))
-        if isinstance(principal, DirectoryPrincipalResource):
-            userID = principal.record.guid
-    except:
-        # TODO: better error handling
-        pass
-
-    try:
-        access = (yield getWikiAccess(userID, wikiID))
-
-        # The ACL we returns has ACEs for the end-user and the wiki principal
-        # in case authzUser is the wiki principal.
-        if access == "read":
-            request.wikiACL = davxml.ACL(
-                davxml.ACE(
-                    request.authnUser,
-                    davxml.Grant(
-                        davxml.Privilege(davxml.Read()),
-                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-
-                        # We allow write-properties so that direct sharees can change
-                        # e.g. calendar color properties
-                        davxml.Privilege(davxml.WriteProperties()),
-                    ),
-                    TwistedACLInheritable(),
-                ),
-                davxml.ACE(
-                    davxml.Principal(
-                        davxml.HRef.fromString("/principals/wikis/%s/" % (wikiID,))
-                    ),
-                    davxml.Grant(
-                        davxml.Privilege(davxml.Read()),
-                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-                    ),
-                    TwistedACLInheritable(),
-                )
-            )
-            returnValue(request.wikiACL)
-
-        elif access in ("write", "admin"):
-            request.wikiACL = davxml.ACL(
-                davxml.ACE(
-                    request.authnUser,
-                    davxml.Grant(
-                        davxml.Privilege(davxml.Read()),
-                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-                        davxml.Privilege(davxml.Write()),
-                    ),
-                    TwistedACLInheritable(),
-                ),
-                davxml.ACE(
-                    davxml.Principal(
-                        davxml.HRef.fromString("/principals/wikis/%s/" % (wikiID,))
-                    ),
-                    davxml.Grant(
-                        davxml.Privilege(davxml.Read()),
-                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
-                        davxml.Privilege(davxml.Write()),
-                    ),
-                    TwistedACLInheritable(),
-                )
-            )
-            returnValue(request.wikiACL)
-
-        else: # "no-access":
-
-            if userID == "unauthenticated":
-                # Return a 401 so they have an opportunity to log in
-                response = (yield UnauthorizedResponse.makeResponse(
-                    request.credentialFactories,
-                    request.remoteAddr,
-                ))
-                raise HTTPError(response)
-
-            raise HTTPError(
-                StatusResponse(
-                    responsecode.FORBIDDEN,
-                    "You are not allowed to access this wiki"
-                )
-            )
-
-    except HTTPError:
-        # pass through the HTTPError we might have raised above
-        raise
-
-    except Exception, e:
-        log.error("Wiki ACL lookup failed: %s" % (e,))
-        raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, "Wiki ACL lookup failed"))

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlaccountsparser.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlaccountsparser.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,283 +0,0 @@
-##
-# Copyright (c) 2006-2014 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.
-##
-
-
-"""
-XML based user/group/resource configuration file handling.
-"""
-
-__all__ = [
-    "XMLAccountsParser",
-]
-
-from twext.python.filepath import CachingFilePath as FilePath
-
-from twext.python.log import Logger
-
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.util import normalizeUUID
-from twistedcaldav.xmlutil import readXML
-
-import re
-import hashlib
-
-log = Logger()
-
-ELEMENT_ACCOUNTS = "accounts"
-ELEMENT_USER = "user"
-ELEMENT_GROUP = "group"
-ELEMENT_LOCATION = "location"
-ELEMENT_RESOURCE = "resource"
-ELEMENT_ADDRESS = "address"
-
-ELEMENT_SHORTNAME = "uid"
-ELEMENT_GUID = "guid"
-ELEMENT_PASSWORD = "password"
-ELEMENT_NAME = "name"
-ELEMENT_FIRST_NAME = "first-name"
-ELEMENT_LAST_NAME = "last-name"
-ELEMENT_EMAIL_ADDRESS = "email-address"
-ELEMENT_MEMBERS = "members"
-ELEMENT_MEMBER = "member"
-ELEMENT_EXTRAS = "extras"
-
-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,
-    ELEMENT_LOCATION : DirectoryService.recordType_locations,
-    ELEMENT_RESOURCE : DirectoryService.recordType_resources,
-    ELEMENT_ADDRESS  : DirectoryService.recordType_addresses,
-}
-
-class XMLAccountsParser(object):
-    """
-    XML account configuration file parser.
-    """
-    def __repr__(self):
-        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
-
-
-    def __init__(self, xmlFile, externalUpdate=True):
-
-        if type(xmlFile) is str:
-            xmlFile = FilePath(xmlFile)
-
-        self.xmlFile = xmlFile
-        self.realm = None
-        self.items = {}
-
-        for recordType in RECORD_TYPES.values():
-            self.items[recordType] = {}
-
-        # Read in XML
-        try:
-            _ignore_tree, accounts_node = readXML(self.xmlFile.path, ELEMENT_ACCOUNTS)
-        except ValueError, e:
-            raise RuntimeError("XML parse error for '%s' because: %s" % (self.xmlFile, e,))
-        self._parseXML(accounts_node)
-
-
-    def _parseXML(self, node):
-        """
-        Parse the XML root node from the accounts configuration document.
-        @param node: the L{Node} to parse.
-        """
-        self.realm = node.get(ATTRIBUTE_REALM, "").encode("utf-8")
-
-        def updateMembership(group):
-            # Update group membership
-            for recordType, shortName in group.members:
-                item = self.items[recordType].get(shortName)
-                if item is not None:
-                    item.groups.add(group.shortNames[0])
-
-        for child in node:
-            try:
-                recordType = RECORD_TYPES[child.tag]
-            except KeyError:
-                raise RuntimeError("Unknown account type: %s" % (child.tag,))
-
-            repeat = int(child.get(ATTRIBUTE_REPEAT, 0))
-
-            principal = XMLAccountRecord(recordType)
-            principal.parseXML(child)
-            if repeat > 0:
-                for i in xrange(1, repeat + 1):
-                    newprincipal = principal.repeat(i)
-                    self.items[recordType][newprincipal.shortNames[0]] = newprincipal
-            else:
-                self.items[recordType][principal.shortNames[0]] = principal
-
-        # Do reverse membership mapping only after all records have been read in
-        for records in self.items.itervalues():
-            for principal in records.itervalues():
-                updateMembership(principal)
-
-
-
-class XMLAccountRecord (object):
-    """
-    Contains provision information for one user.
-    """
-    def __init__(self, recordType):
-        """
-        @param recordType: record type for directory entry.
-        """
-        self.recordType = recordType
-        self.shortNames = []
-        self.guid = None
-        self.password = None
-        self.fullName = None
-        self.firstName = None
-        self.lastName = None
-        self.emailAddresses = set()
-        self.members = set()
-        self.groups = set()
-        self.extras = {}
-
-
-    def repeat(self, ctr):
-        """
-        Create another object like this but with all text items having % substitution
-        done on them with the numeric value provided.
-        @param ctr: an integer to substitute into text.
-        """
-
-        # Regular expression which matches ~ followed by a number
-        matchNumber = re.compile(r"(~\d+)")
-
-        def expand(text, ctr):
-            """
-            Returns a string where ~<number> is replaced by the first <number>
-            characters from the md5 hexdigest of str(ctr), e.g.::
-
-                expand("~9 foo", 1)
-
-            returns::
-
-                "c4ca4238a foo"
-
-            ...since "c4ca4238a" is the first 9 characters of::
-
-                hashlib.md5(str(1)).hexdigest()
-
-            If <number> is larger than 32, the hash will repeat as needed.
-            """
-            if text:
-                m = matchNumber.search(text)
-                if m:
-                    length = int(m.group(0)[1:])
-                    hash = hashlib.md5(str(ctr)).hexdigest()
-                    string = (hash * ((length / 32) + 1))[:-(32 - (length % 32))]
-                    return text.replace(m.group(0), string)
-            return text
-
-        shortNames = []
-        for shortName in self.shortNames:
-            if shortName.find("%") != -1:
-                shortNames.append(shortName % ctr)
-            else:
-                shortNames.append(shortName)
-        if self.guid and self.guid.find("%") != -1:
-            guid = self.guid % ctr
-        else:
-            guid = self.guid
-        if self.password.find("%") != -1:
-            password = self.password % ctr
-        else:
-            password = self.password
-        if self.fullName.find("%") != -1:
-            fullName = self.fullName % ctr
-        else:
-            fullName = self.fullName
-        fullName = expand(fullName, ctr)
-        if self.firstName and self.firstName.find("%") != -1:
-            firstName = self.firstName % ctr
-        else:
-            firstName = self.firstName
-        firstName = expand(firstName, ctr)
-        if self.lastName and self.lastName.find("%") != -1:
-            lastName = self.lastName % ctr
-        else:
-            lastName = self.lastName
-        lastName = expand(lastName, ctr)
-        emailAddresses = set()
-        for emailAddr in self.emailAddresses:
-            emailAddr = expand(emailAddr, ctr)
-            if emailAddr.find("%") != -1:
-                emailAddresses.add(emailAddr % ctr)
-            else:
-                emailAddresses.add(emailAddr)
-
-        result = XMLAccountRecord(self.recordType)
-        result.shortNames = shortNames
-        result.guid = normalizeUUID(guid)
-        result.password = password
-        result.fullName = fullName
-        result.firstName = firstName
-        result.lastName = lastName
-        result.emailAddresses = emailAddresses
-        result.members = self.members
-        result.extras = self.extras
-        return result
-
-
-    def parseXML(self, node):
-        for child in node:
-            if child.tag == ELEMENT_SHORTNAME:
-                self.shortNames.append(child.text.encode("utf-8"))
-            elif child.tag == ELEMENT_GUID:
-                self.guid = normalizeUUID(child.text.encode("utf-8"))
-                if len(self.guid) < 4:
-                    self.guid += "?" * (4 - len(self.guid))
-            elif child.tag == ELEMENT_PASSWORD:
-                self.password = child.text.encode("utf-8")
-            elif child.tag == ELEMENT_NAME:
-                self.fullName = child.text.encode("utf-8")
-            elif child.tag == ELEMENT_FIRST_NAME:
-                self.firstName = child.text.encode("utf-8")
-            elif child.tag == ELEMENT_LAST_NAME:
-                self.lastName = child.text.encode("utf-8")
-            elif child.tag == ELEMENT_EMAIL_ADDRESS:
-                self.emailAddresses.add(child.text.encode("utf-8").lower())
-            elif child.tag == ELEMENT_MEMBERS:
-                self._parseMembers(child, self.members)
-            elif child.tag == ELEMENT_EXTRAS:
-                self._parseExtras(child, self.extras)
-            else:
-                raise RuntimeError("Unknown account attribute: %s" % (child.tag,))
-
-        if not self.shortNames:
-            self.shortNames.append(self.guid)
-
-
-    def _parseMembers(self, node, addto):
-        for child in node:
-            if child.tag == ELEMENT_MEMBER:
-                recordType = child.get(ATTRIBUTE_RECORDTYPE, DirectoryService.recordType_users)
-                addto.add((recordType, child.text.encode("utf-8")))
-
-
-    def _parseExtras(self, node, addto):
-        for child in node:
-            addto[child.tag] = child.text.encode("utf-8")

Deleted: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlfile.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directory/xmlfile.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,633 +0,0 @@
-##
-# Copyright (c) 2006-2014 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.
-##
-
-"""
-XML based user/group/resource directory service implementation.
-"""
-
-__all__ = [
-    "XMLDirectoryService",
-]
-
-from time import time
-import grp
-import os
-import pwd
-import types
-
-from twisted.cred.credentials import UsernamePassword
-from txweb2.auth.digest import DigestedCredentials
-from twext.python.filepath import CachingFilePath as FilePath
-from twistedcaldav.config import config
-from twisted.internet.defer import succeed
-
-from twistedcaldav.config import fullServerPath
-from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, DirectoryError
-from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser, XMLAccountRecord
-from twistedcaldav.directory.util import normalizeUUID
-from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
-from twistedcaldav.xmlutil import addSubElement, createElement, elementToXML
-from uuid import uuid4
-
-
-class XMLDirectoryService(DirectoryService):
-    """
-    XML based implementation of L{IDirectoryService}.
-    """
-    baseGUID = "9CA8DEC5-5A17-43A9-84A8-BE77C1FB9172"
-
-    realmName = None
-
-    INDEX_TYPE_GUID = "guid"
-    INDEX_TYPE_SHORTNAME = "shortname"
-    INDEX_TYPE_CUA = "cua"
-    INDEX_TYPE_AUTHID = "authid"
-
-
-    def __repr__(self):
-        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.xmlFile)
-
-
-    def __init__(self, params, alwaysStat=False):
-
-        defaults = {
-            'xmlFile' : None,
-            'directoryBackedAddressBook': None,
-            'recordTypes' : (
-                self.recordType_users,
-                self.recordType_groups,
-                self.recordType_locations,
-                self.recordType_resources,
-                self.recordType_addresses,
-            ),
-            'realmName' : '/Search',
-            'statSeconds' : 15,
-            'augmentService' : None,
-            'groupMembershipCache' : None,
-        }
-        ignored = None
-        params = self.getParams(params, defaults, ignored)
-
-        self._recordTypes = params['recordTypes']
-        self.realmName = params['realmName']
-        self.statSeconds = params['statSeconds']
-        self.augmentService = params['augmentService']
-        self.groupMembershipCache = params['groupMembershipCache']
-
-        super(XMLDirectoryService, self).__init__()
-
-        xmlFile = fullServerPath(config.DataRoot, params.get("xmlFile"))
-        if type(xmlFile) is str:
-            xmlFile = FilePath(xmlFile)
-
-        if not xmlFile.exists():
-            xmlFile.setContent("""<?xml version="1.0" encoding="utf-8"?>
-
-<accounts realm="%s">
-</accounts>
-""" % (self.realmName,))
-
-        uid = -1
-        if config.UserName:
-            try:
-                uid = pwd.getpwnam(config.UserName).pw_uid
-            except KeyError:
-                self.log.error("User not found: %s" % (config.UserName,))
-
-        gid = -1
-        if config.GroupName:
-            try:
-                gid = grp.getgrnam(config.GroupName).gr_gid
-            except KeyError:
-                self.log.error("Group not found: %s" % (config.GroupName,))
-
-        if uid != -1 and gid != -1:
-            os.chown(xmlFile.path, uid, gid)
-
-        self.xmlFile = xmlFile
-        self._fileInfo = None
-        self._lastCheck = 0
-        self._alwaysStat = alwaysStat
-        self.directoryBackedAddressBook = params.get('directoryBackedAddressBook')
-        self._initIndexes()
-        self._accounts()
-
-
-    def _initIndexes(self):
-        """
-        Create empty indexes
-        """
-        self.records = {}
-        self.recordIndexes = {}
-
-        for recordType in self.recordTypes():
-            self.records[recordType] = set()
-            self.recordIndexes[recordType] = {
-                self.INDEX_TYPE_GUID     : {},
-                self.INDEX_TYPE_SHORTNAME: {},
-                self.INDEX_TYPE_CUA      : {},
-                self.INDEX_TYPE_AUTHID   : {},
-            }
-
-
-    def _accounts(self):
-        """
-        Parses XML file, creates XMLDirectoryRecords and indexes them, and
-        because some other code in this module still works directly with
-        XMLAccountRecords as returned by XMLAccountsParser, returns a list
-        of XMLAccountRecords.
-
-        The XML file is only stat'ed at most every self.statSeconds, and is
-        only reparsed if it's been modified.
-
-        FIXME: don't return XMLAccountRecords, and have any code in this module
-        which currently does work with XMLAccountRecords, modify such code to
-        use XMLDirectoryRecords instead.
-        """
-        currentTime = time()
-        if self._alwaysStat or currentTime - self._lastCheck > self.statSeconds:
-            self.xmlFile.restat()
-            self._lastCheck = currentTime
-            fileInfo = (self.xmlFile.getmtime(), self.xmlFile.getsize())
-            if fileInfo != self._fileInfo:
-                self._initIndexes()
-                parser = XMLAccountsParser(self.xmlFile)
-                self._parsedAccounts = parser.items
-                self.realmName = parser.realm
-                self._fileInfo = fileInfo
-
-                for accountDict in self._parsedAccounts.itervalues():
-                    for xmlAccountRecord in accountDict.itervalues():
-                        if xmlAccountRecord.recordType not in self.recordTypes():
-                            continue
-                        record = XMLDirectoryRecord(
-                            service=self,
-                            recordType=xmlAccountRecord.recordType,
-                            shortNames=tuple(xmlAccountRecord.shortNames),
-                            xmlPrincipal=xmlAccountRecord,
-                        )
-                        if self.augmentService is not None:
-                            d = self.augmentService.getAugmentRecord(record.guid,
-                                record.recordType)
-                            d.addCallback(lambda x: record.addAugmentInformation(x))
-
-                        self._addToIndex(record)
-
-        return self._parsedAccounts
-
-
-    def _addToIndex(self, record):
-        """
-        Index the record by GUID, shortName(s), authID(s) and CUA(s)
-        """
-
-        self.recordIndexes[record.recordType][self.INDEX_TYPE_GUID][record.guid] = record
-        for shortName in record.shortNames:
-            self.recordIndexes[record.recordType][self.INDEX_TYPE_SHORTNAME][shortName] = record
-        for authID in record.authIDs:
-            self.recordIndexes[record.recordType][self.INDEX_TYPE_AUTHID][authID] = record
-        for cua in record.calendarUserAddresses:
-            cua = normalizeCUAddr(cua)
-            self.recordIndexes[record.recordType][self.INDEX_TYPE_CUA][cua] = record
-        self.records[record.recordType].add(record)
-
-
-    def _removeFromIndex(self, record):
-        """
-        Removes a record from all indexes.  Note this is only used for unit
-        testing, to simulate a user being removed from the directory.
-        """
-        del self.recordIndexes[record.recordType][self.INDEX_TYPE_GUID][record.guid]
-        for shortName in record.shortNames:
-            del self.recordIndexes[record.recordType][self.INDEX_TYPE_SHORTNAME][shortName]
-        for authID in record.authIDs:
-            del self.recordIndexes[record.recordType][self.INDEX_TYPE_AUTHID][authID]
-        for cua in record.calendarUserAddresses:
-            cua = normalizeCUAddr(cua)
-            del self.recordIndexes[record.recordType][self.INDEX_TYPE_CUA][cua]
-        if record in self.records[record.recordType]:
-            self.records[record.recordType].remove(record)
-
-
-    def _lookupInIndex(self, recordType, indexType, key):
-        """
-        Look for an existing record of the given recordType with the key for
-        the given index type.  Returns None if no match.
-        """
-        self._accounts()
-        return self.recordIndexes.get(recordType, {}).get(indexType, {}).get(key, None)
-
-
-    def _initCaches(self):
-        """
-        Invalidates the indexes
-        """
-        self._lastCheck = 0
-        self._initIndexes()
-
-
-    def _forceReload(self):
-        """
-        Invalidates the indexes, re-reads the XML file and re-indexes
-        """
-        self._initCaches()
-        self._fileInfo = None
-        return self._accounts()
-
-
-    def recordWithShortName(self, recordType, shortName):
-        return self._lookupInIndex(recordType, self.INDEX_TYPE_SHORTNAME, shortName)
-
-
-    def recordWithAuthID(self, authID):
-        for recordType in self.recordTypes():
-            record = self._lookupInIndex(recordType, self.INDEX_TYPE_AUTHID, authID)
-            if record is not None:
-                return record
-        return None
-
-
-    def recordWithGUID(self, guid):
-        guid = normalizeUUID(guid)
-        for recordType in self.recordTypes():
-            record = self._lookupInIndex(recordType, self.INDEX_TYPE_GUID, guid)
-            if record is not None:
-                return record
-        return None
-
-    recordWithUID = recordWithGUID
-
-    def createCache(self):
-        """
-        No-op to pacify addressbook backing.
-        """
-
-    def recordTypes(self):
-        return self._recordTypes
-
-
-    def listRecords(self, recordType):
-        self._accounts()
-        return self.records[recordType]
-
-
-    def recordsMatchingFields(self, fields, operand="or", recordType=None):
-        # Default, brute force method search of underlying XML data
-
-        def fieldMatches(fieldValue, value, caseless, matchType):
-            if fieldValue is None:
-                return False
-            elif type(fieldValue) in types.StringTypes:
-                fieldValue = (fieldValue,)
-
-            for testValue in fieldValue:
-                if caseless:
-                    testValue = testValue.lower()
-                    value = value.lower()
-
-                if matchType == 'starts-with':
-                    if testValue.startswith(value):
-                        return True
-                elif matchType == 'contains':
-                    try:
-                        testValue.index(value)
-                        return True
-                    except ValueError:
-                        pass
-                else: # exact
-                    if testValue == value:
-                        return True
-
-            return False
-
-        def xmlPrincipalMatches(xmlPrincipal):
-            if operand == "and":
-                for fieldName, value, caseless, matchType in fields:
-                    try:
-                        fieldValue = getattr(xmlPrincipal, fieldName)
-                        if not fieldMatches(fieldValue, value, caseless, matchType):
-                            return False
-                    except AttributeError:
-                        # No property => no match
-                        return False
-                # we hit on every property
-                return True
-            else: # "or"
-                for fieldName, value, caseless, matchType in fields:
-                    try:
-                        fieldValue = getattr(xmlPrincipal, fieldName)
-                        if fieldMatches(fieldValue, value, caseless, matchType):
-                            return True
-                    except AttributeError:
-                        # No value
-                        pass
-                # we didn't hit any
-                return False
-
-        if recordType is None:
-            recordTypes = list(self.recordTypes())
-        else:
-            recordTypes = (recordType,)
-
-        records = []
-        for recordType in recordTypes:
-            for xmlPrincipal in self._accounts()[recordType].itervalues():
-                if xmlPrincipalMatches(xmlPrincipal):
-
-                    # Load/cache record from its GUID
-                    record = self.recordWithGUID(xmlPrincipal.guid)
-                    if record:
-                        records.append(record)
-        return succeed(records)
-
-
-    def _addElement(self, parent, principal):
-        """
-        Create an XML element from principal and add it as a child of parent
-        """
-
-        # TODO: derive this from xmlaccountsparser.py
-        xmlTypes = {
-            'users'     : 'user',
-            'groups'    : 'group',
-            'locations' : 'location',
-            'resources' : 'resource',
-            'addresses' : 'address',
-        }
-        xmlType = xmlTypes[principal.recordType]
-
-        element = addSubElement(parent, xmlType)
-        for value in principal.shortNames:
-            addSubElement(element, "uid", text=value.decode("utf-8"))
-        addSubElement(element, "guid", text=principal.guid)
-        if principal.fullName is not None:
-            addSubElement(element, "name", text=principal.fullName.decode("utf-8"))
-        if principal.firstName is not None:
-            addSubElement(element, "first-name", text=principal.firstName.decode("utf-8"))
-        if principal.lastName is not None:
-            addSubElement(element, "last-name", text=principal.lastName.decode("utf-8"))
-        for value in principal.emailAddresses:
-            addSubElement(element, "email-address", text=value.decode("utf-8"))
-        if principal.extras:
-            extrasElement = addSubElement(element, "extras")
-            for key, value in principal.extras.iteritems():
-                addSubElement(extrasElement, key, text=value.decode("utf-8"))
-
-        return element
-
-
-    def _persistRecords(self, element):
-
-        def indent(elem, level=0):
-            i = "\n" + level * "  "
-            if len(elem):
-                if not elem.text or not elem.text.strip():
-                    elem.text = i + "  "
-                if not elem.tail or not elem.tail.strip():
-                    elem.tail = i
-                for elem in elem:
-                    indent(elem, level + 1)
-                if not elem.tail or not elem.tail.strip():
-                    elem.tail = i
-            else:
-                if level and (not elem.tail or not elem.tail.strip()):
-                    elem.tail = i
-
-        indent(element)
-
-        self.xmlFile.setContent(elementToXML(element))
-
-        # Fix up the file ownership because setContent doesn't maintain it
-        uid = -1
-        if config.UserName:
-            try:
-                uid = pwd.getpwnam(config.UserName).pw_uid
-            except KeyError:
-                self.log.error("User not found: %s" % (config.UserName,))
-
-        gid = -1
-        if config.GroupName:
-            try:
-                gid = grp.getgrnam(config.GroupName).gr_gid
-            except KeyError:
-                self.log.error("Group not found: %s" % (config.GroupName,))
-
-        if uid != -1 and gid != -1:
-            os.chown(self.xmlFile.path, uid, gid)
-
-
-    def createRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, password=None, **kwargs):
-        """
-        Create and persist a record using the provided information.  In this
-        XML-based implementation, the xml accounts are read in and converted
-        to elementtree elements, a new element is added for the new record,
-        and the document is serialized to disk.
-        """
-        if guid is None:
-            guid = str(uuid4())
-        guid = normalizeUUID(guid)
-
-        if not shortNames:
-            shortNames = (guid,)
-
-        # Make sure latest XML records are read in
-        accounts = self._forceReload()
-
-        accountsElement = createElement("accounts", realm=self.realmName)
-        for recType in self.recordTypes():
-            for xmlPrincipal in accounts[recType].itervalues():
-                if xmlPrincipal.guid == guid:
-                    raise DirectoryError("Duplicate guid: %s" % (guid,))
-                for shortName in shortNames:
-                    if shortName in xmlPrincipal.shortNames:
-                        raise DirectoryError("Duplicate shortName: %s" %
-                            (shortName,))
-                self._addElement(accountsElement, xmlPrincipal)
-
-        xmlPrincipal = XMLAccountRecord(recordType)
-        xmlPrincipal.shortNames = shortNames
-        xmlPrincipal.guid = guid
-        xmlPrincipal.password = password
-        xmlPrincipal.fullName = fullName
-        xmlPrincipal.firstName = firstName
-        xmlPrincipal.lastName = lastName
-        xmlPrincipal.emailAddresses = emailAddresses
-        xmlPrincipal.extras = kwargs
-        self._addElement(accountsElement, xmlPrincipal)
-
-        self._persistRecords(accountsElement)
-        self._forceReload()
-        return self.recordWithGUID(guid)
-
-
-    def destroyRecord(self, recordType, guid=None):
-        """
-        Remove the record matching guid.  In this XML-based implementation,
-        the xml accounts are read in and those not matching the given guid are
-        converted to elementtree elements, then the document is serialized to
-        disk.
-        """
-
-        guid = normalizeUUID(guid)
-
-        # Make sure latest XML records are read in
-        accounts = self._forceReload()
-
-        accountsElement = createElement("accounts", realm=self.realmName)
-        for recType in self.recordTypes():
-
-            for xmlPrincipal in accounts[recType].itervalues():
-                if xmlPrincipal.guid != guid:
-                    self._addElement(accountsElement, xmlPrincipal)
-
-        self._persistRecords(accountsElement)
-        self._forceReload()
-
-
-    def updateRecord(self, recordType, guid=None, shortNames=(), authIDs=set(),
-        fullName=None, firstName=None, lastName=None, emailAddresses=set(),
-        uid=None, password=None, **kwargs):
-        """
-        Update the record matching guid.  In this XML-based implementation,
-        the xml accounts are read in and converted to elementtree elements.
-        The account matching the given guid is replaced, then the document
-        is serialized to disk.
-        """
-
-        guid = normalizeUUID(guid)
-
-        # Make sure latest XML records are read in
-        accounts = self._forceReload()
-
-        accountsElement = createElement("accounts", realm=self.realmName)
-        for recType in self.recordTypes():
-
-            for xmlPrincipal in accounts[recType].itervalues():
-                if xmlPrincipal.guid == guid:
-                    # Replace this record
-                    xmlPrincipal.shortNames = shortNames
-                    xmlPrincipal.password = password
-                    xmlPrincipal.fullName = fullName
-                    xmlPrincipal.firstName = firstName
-                    xmlPrincipal.lastName = lastName
-                    xmlPrincipal.emailAddresses = emailAddresses
-                    xmlPrincipal.extras = kwargs
-                    self._addElement(accountsElement, xmlPrincipal)
-                else:
-                    self._addElement(accountsElement, xmlPrincipal)
-
-        self._persistRecords(accountsElement)
-        self._forceReload()
-        return self.recordWithGUID(guid)
-
-
-    def createRecords(self, data):
-        """
-        Create records in bulk
-        """
-
-        # Make sure latest XML records are read in
-        accounts = self._forceReload()
-
-        knownGUIDs = {}
-        knownShortNames = {}
-
-        accountsElement = createElement("accounts", realm=self.realmName)
-        for recType in self.recordTypes():
-            for xmlPrincipal in accounts[recType].itervalues():
-                self._addElement(accountsElement, xmlPrincipal)
-                knownGUIDs[xmlPrincipal.guid] = 1
-                for shortName in xmlPrincipal.shortNames:
-                    knownShortNames[shortName] = 1
-
-        for recordType, recordData in data:
-            guid = recordData["guid"]
-            if guid is None:
-                guid = str(uuid4())
-
-            shortNames = recordData["shortNames"]
-            if not shortNames:
-                shortNames = (guid,)
-
-            if guid in knownGUIDs:
-                raise DirectoryError("Duplicate guid: %s" % (guid,))
-
-            for shortName in shortNames:
-                if shortName in knownShortNames:
-                    raise DirectoryError("Duplicate shortName: %s" %
-                        (shortName,))
-
-            xmlPrincipal = XMLAccountRecord(recordType)
-            xmlPrincipal.shortNames = shortNames
-            xmlPrincipal.guid = guid
-            xmlPrincipal.fullName = recordData["fullName"]
-            self._addElement(accountsElement, xmlPrincipal)
-
-        self._persistRecords(accountsElement)
-        self._forceReload()
-
-
-
-class XMLDirectoryRecord(DirectoryRecord):
-    """
-    XML based implementation implementation of L{IDirectoryRecord}.
-    """
-    def __init__(self, service, recordType, shortNames, xmlPrincipal):
-        super(XMLDirectoryRecord, self).__init__(
-            service=service,
-            recordType=recordType,
-            guid=xmlPrincipal.guid,
-            shortNames=shortNames,
-            fullName=xmlPrincipal.fullName,
-            firstName=xmlPrincipal.firstName,
-            lastName=xmlPrincipal.lastName,
-            emailAddresses=xmlPrincipal.emailAddresses,
-            **xmlPrincipal.extras
-        )
-
-        self.password = xmlPrincipal.password
-        self._members = xmlPrincipal.members
-        self._groups = xmlPrincipal.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 memberGUIDs(self):
-        results = set()
-        for recordType, shortName in self._members:
-            record = self.service.recordWithShortName(recordType, shortName)
-            results.add(record.guid)
-        return results
-
-
-    def verifyCredentials(self, credentials):
-        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/sagen/move2who-5/twistedcaldav/directorybackedaddressbook.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directorybackedaddressbook.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/directorybackedaddressbook.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -22,89 +22,72 @@
     "DirectoryBackedAddressBookResource",
 ]
 
+
 from twext.python.log import Logger
+from twext.who.expression import Operand, MatchType, MatchFlags, \
+    MatchExpression, CompoundExpression
+from twext.who.idirectory import FieldName
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import succeed, inlineCallbacks, maybeDeferred, \
+    returnValue
+from twisted.python.constants import NamedConstant
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.resource import CalDAVResource
+from txdav.carddav.datastore.query.filter import IsNotDefined, TextMatch, \
+    ParameterFilter
+from txdav.who.idirectory import FieldName as CalFieldName
+from txdav.who.vcard import vCardKindToRecordTypeMap, recordTypeToVCardKindMap, \
+    vCardPropToParamMap, vCardConstantProperties, vCardFromRecord
+from txdav.xml import element as davxml
+from txdav.xml.base import twisted_dav_namespace, dav_namespace, parse_date, \
+    twisted_private_namespace
 from txweb2 import responsecode
-from txdav.xml import element as davxml
+from txweb2.dav.resource import DAVPropertyMixIn
 from txweb2.dav.resource import TwistedACLInheritable
+from txweb2.dav.util import joinURL
 from txweb2.http import HTTPError, StatusResponse
-
-from twisted.internet.defer import succeed, inlineCallbacks, maybeDeferred, returnValue
-from twisted.python.reflect import namedClass
-
-from twistedcaldav.config import config
-from twistedcaldav.resource import CalDAVResource
-
+from txweb2.http_headers import MimeType, generateContentType, ETag
+from xmlrpclib import datetime
+import hashlib
 import uuid
 
 log = Logger()
 
+MatchFlags_none = MatchFlags.NOT & ~MatchFlags.NOT  # can't import MatchFlags_none
 
-
 class DirectoryBackedAddressBookResource (CalDAVResource):
     """
     Directory-backed address book
     """
 
-    def __init__(self, principalCollections):
+    def __init__(self, principalCollections, principalDirectory, uri):
 
         CalDAVResource.__init__(self, principalCollections=principalCollections)
 
-        self.directory = None       # creates directory attribute
+        self.principalDirectory = principalDirectory
+        self.uri = uri
+        self.directory = None
 
-        # create with permissions, similar to CardDAVOptions in tap.py
-        # FIXME:  /Directory does not need to be in file system unless debug-only caching options are used
-#        try:
-#            os.mkdir(path)
-#            os.chmod(path, 0750)
-#            if config.UserName and config.GroupName:
-#                import pwd
-#                import grp
-#                uid = pwd.getpwnam(config.UserName)[2]
-#                gid = grp.getgrnam(config.GroupName)[2]
-#                os.chown(path, uid, gid)
-#
-#            log.info("Created %s" % (path,))
-#
-#        except (OSError,), e:
-#            # this is caused by multiprocessor race and is harmless
-#            if e.errno != errno.EEXIST:
-#                raise
 
-
     def makeChild(self, name):
         from twistedcaldav.simpleresource import SimpleCalDAVResource
         return SimpleCalDAVResource(principalCollections=self.principalCollections())
+        return self.directory
 
 
     def provisionDirectory(self):
         if self.directory is None:
-            directoryClass = namedClass(config.DirectoryAddressBook.type)
-
             log.info(
-                "Configuring: {t}:{p}",
-                t=config.DirectoryAddressBook.type,
-                p=config.DirectoryAddressBook.params,
-            )
+                "Setting search directory to {principalDirectory}",
+                principalDirectory=self.principalDirectory)
+            self.directory = self.principalDirectory
+            # future: instantiate another directory based on /Search/Contacts (?)
 
-            #add self as "directoryBackedAddressBook" parameter
-            params = config.DirectoryAddressBook.params.copy()
-            params["directoryBackedAddressBook"] = self
-
-            try:
-                self.directory = directoryClass(params)
-            except ImportError, e:
-                log.error("Unable to set up directory address book: %s" % (e,))
-                return succeed(None)
-
-            return self.directory.createCache()
-
-            #print ("DirectoryBackedAddressBookResource.provisionDirectory: provisioned")
-
         return succeed(None)
 
 
     def defaultAccessControlList(self):
-        #print( "DirectoryBackedAddressBookResource.defaultAccessControlList" )
         if config.AnonymousDirectoryAddressBookAccess:
             # DAV:Read for all principals (includes anonymous)
             accessPrincipal = davxml.All()
@@ -112,16 +95,18 @@
             # DAV:Read for all authenticated principals (does not include anonymous)
             accessPrincipal = davxml.Authenticated()
 
-        return davxml.ACL(
-            davxml.ACE(
-                davxml.Principal(accessPrincipal),
-                davxml.Grant(
-                    davxml.Privilege(davxml.Read()),
-                    davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet())
-                                ),
-                davxml.Protected(),
-                TwistedACLInheritable(),
-           ),
+        return succeed(
+            davxml.ACL(
+                davxml.ACE(
+                    davxml.Principal(accessPrincipal),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet())
+                                    ),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+               ),
+            )
         )
 
 
@@ -139,7 +124,7 @@
 
     def resourceID(self):
         if self.directory:
-            resource_id = uuid.uuid5(uuid.UUID("5AAD67BF-86DD-42D7-9161-6AF977E4DAA3"), self.directory.baseGUID).urn
+            resource_id = uuid.uuid5(uuid.UUID("5AAD67BF-86DD-42D7-9161-6AF977E4DAA3"), self.directory.guid).urn
         else:
             resource_id = "tag:unknown"
         return resource_id
@@ -150,7 +135,6 @@
 
 
     def isAddressBookCollection(self):
-        #print( "DirectoryBackedAddressBookResource.isAddressBookCollection: return True" )
         return True
 
 
@@ -160,28 +144,546 @@
 
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
         # Permissions here are fixed, and are not subject to inheritance rules, etc.
-        return succeed(self.defaultAccessControlList())
+        return self.defaultAccessControlList()
 
 
     @inlineCallbacks
     def renderHTTP(self, request):
         if not self.directory:
             raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, "Service is starting up"))
-        elif self.directory.liveQuery:
-            response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
-            returnValue(response)
-        else:
-            available = (yield maybeDeferred(self.directory.available,))
 
-            if not available:
-                raise HTTPError(StatusResponse(responsecode.SERVICE_UNAVAILABLE, "Service is starting up"))
+        response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def doAddressBookDirectoryQuery(self, addressBookFilter, addressBookQuery, maxResults, defaultKind=None):
+        """
+        Get vCards for a given addressBookFilter and addressBookQuery
+        """
+
+        log.debug("doAddressBookDirectoryQuery: directory={directory} addressBookFilter={addressBookFilter}, addressBookQuery={addressBookQuery}, maxResults={maxResults}",
+                  directory=self.directory, addressBookFilter=addressBookFilter, addressBookQuery=addressBookQuery, maxResults=maxResults)
+        results = []
+        limited = False
+        maxQueryRecords = 0
+
+        vcardPropToRecordFieldMap = {
+            "FN": FieldName.fullNames,
+            "N": FieldName.fullNames,
+            "EMAIL": FieldName.emailAddresses,
+            "UID": FieldName.uid,
+            "ADR": (
+                    CalFieldName.streetAddress,
+                    CalFieldName.floor,
+                    ),
+            "KIND": FieldName.recordType,
+            # LATER "X-ADDRESSBOOKSERVER-MEMBER": FieldName.membersUIDs,
+        }
+
+        propNames, expression = expressionFromABFilter(
+            addressBookFilter, vcardPropToRecordFieldMap, vCardConstantProperties
+        )
+
+        if expression:
+            if defaultKind and "KIND" not in propNames:
+                defaultRecordExpression = MatchExpression(
+                    FieldName.recordType,
+                    vCardKindToRecordTypeMap[defaultKind],
+                    MatchType.equals
+                )
+                if expression is True:
+                    expression = defaultRecordExpression
+                else:
+                    expression = CompoundExpression(
+                        (expression, defaultRecordExpression,),
+                        Operand.AND
+                    )
+            elif expression is True: # True means all records
+                allowedRecordTypes = set(self.directory.recordTypes()) & set(recordTypeToVCardKindMap.keys())
+                expression = CompoundExpression(
+                    [
+                        MatchExpression(FieldName.recordType, recordType, MatchType.equals)
+                            for recordType in allowedRecordTypes
+                    ], Operand.OR
+                )
+
+            maxRecords = int(maxResults * 1.2)
+
+            # keep trying query till we get results based on filter.  Especially when doing "all results" query
+            while True:
+
+                log.debug("doAddressBookDirectoryQuery: expression={expression!r}, propNames={propNames}", expression=expression, propNames=propNames)
+
+                records = yield self.directory.recordsFromExpression(expression)
+                log.debug("doAddressBookDirectoryQuery: #records={n}, records={records!r}", n=len(records), records=records)
+                queryLimited = False
+
+                vCardsResults = [(yield ABDirectoryQueryResult(self).generate(record)) for record in records]
+
+                filteredResults = []
+                for vCardResult in vCardsResults:
+                    if addressBookFilter.match(vCardResult.vCard()):
+                        filteredResults.append(vCardResult)
+                    else:
+                        log.debug("doAddressBookDirectoryQuery: vCard did not match filter:\n{vcard}", vcard=vCardResult.vCard())
+
+                #no more results
+                if not queryLimited:
+                    break
+
+                # more than requested results
+                if maxResults and len(filteredResults) >= maxResults:
+                    break
+
+                # more than max report results
+                if len(filteredResults) >= config.MaxQueryWithDataResults:
+                    break
+
+                # more than self limit
+                if maxQueryRecords and maxRecords >= maxQueryRecords:
+                    break
+
+                # try again with 2x
+                maxRecords *= 2
+                if maxQueryRecords and maxRecords > maxQueryRecords:
+                    maxRecords = maxQueryRecords
+
+            results = sorted(list(filteredResults), key=lambda result: result.vCard().propertyValue("UID"))
+            limited = maxResults and len(results) >= maxResults
+
+        log.info("limited={l} #results={n}", l=limited, n=len(results))
+        returnValue((results, limited,))
+
+
+
+def propertiesInAddressBookQuery(addressBookQuery):
+    """
+    Get the vCard properties requested by a given query
+    """
+
+    etagRequested = False
+    propertyNames = []
+    if addressBookQuery.qname() == ("DAV:", "prop"):
+
+        for property in addressBookQuery.children:
+            if isinstance(property, carddavxml.AddressData):
+                for addressProperty in property.children:
+                    if isinstance(addressProperty, carddavxml.Property):
+                        propertyNames.append(addressProperty.attributes["name"])
+
+            elif property.qname() == ("DAV:", "getetag"):
+                # for a real etag == md5(vCard), we need all properties
+                etagRequested = True
+
+    return (etagRequested, propertyNames if len(propertyNames) else None)
+
+
+
+def expressionFromABFilter(addressBookFilter, vcardPropToSearchableFieldMap, constantProperties={}):
+    """
+    Convert the supplied addressbook-query into a ds expression tree.
+
+    @param addressBookFilter: the L{Filter} for the addressbook-query to convert.
+    @param vcardPropToSearchableFieldMap: a mapping from vcard properties to searchable query attributes.
+    @param constantProperties: a mapping of constant properties.  A query on a constant property will return all or None
+    @return: (filterProperyNames, expressions) tuple.  expression==True means list all results, expression==False means no results
+    """
+
+    def propFilterListQuery(filterAllOf, propFilters):
+
+        """
+        Create an expression for a list of prop-filter elements.
+
+        @param filterAllOf: the C{True} if parent filter test is "allof"
+        @param propFilters: the C{list} of L{ComponentFilter} elements.
+        @return: (filterProperyNames, expressions) tuple.  expression==True means list all results, expression==False means no results
+        """
+
+        def combineExpressionLists(expressionList, allOf, addedExpressions):
+            """
+            deal with the 4-state logic
+                addedExpressions=None means ignore
+                addedExpressions=True means all records
+                addedExpressions=False means no records
+                addedExpressions=[expressionlist] add to expression list
+            """
+            if expressionList is None:
+                expressionList = addedExpressions
+            elif addedExpressions is not None:
+                if addedExpressions is True:
+                    if not allOf:
+                        expressionList = True  # expressionList or True is True
+                    #else  expressionList and True is expressionList
+                elif addedExpressions is False:
+                    if allOf:
+                        expressionList = False  # expressionList and False is False
+                    #else expressionList or False is expressionList
+                else:
+                    if expressionList is False:
+                        if not allOf:
+                            expressionList = addedExpressions  # False or addedExpressions is addedExpressions
+                        #else False and addedExpressions is False
+                    elif expressionList is True:
+                        if allOf:
+                            expressionList = addedExpressions  # False or addedExpressions is addedExpressions
+                        #else False and addedExpressions is False
+                    else:
+                        expressionList.extend(addedExpressions)
+            return expressionList
+
+
+        def propFilterExpression(filterAllOf, propFilter):
+            """
+            Create an expression for a single prop-filter element.
+
+            @param propFilter: the L{PropertyFilter} element.
+            @return: (filterProperyNames, expressions) tuple.  expression==True means list all results, expression==False means no results
+            """
+
+            def matchExpression(fieldName, matchString, matchType, matchFlags):
+                # special case recordType field
+                if fieldName == FieldName.recordType:
+                    # change kind to record type
+                    matchValue = vCardKindToRecordTypeMap.get(matchString.lower())
+                    if matchValue is None:
+                        matchValue = NamedConstant()
+                        matchValue.description = u""
+
+                    # change types and flags
+                    matchFlags &= ~MatchFlags.caseInsensitive
+                    matchType = MatchType.equals
+                else:
+                    matchValue = matchString.decode("utf-8")
+
+                return MatchExpression(fieldName, matchValue, matchType, matchFlags)
+
+
+            def definedExpression(defined, allOf):
+                if constant or propFilter.filter_name in ("N" , "FN", "UID", "SOURCE", "KIND",):
+                    return defined  # all records have this property so no records do not have it
+                else:
+                    # FIXME: The startsWith expression below, which works with LDAP and OD. is not currently supported
+                    return True
+                    '''
+                    # this may generate inefficient LDAP query string
+                    matchFlags = MatchFlags_none if defined else MatchFlags.NOT
+                    matchList = [matchExpression(fieldName, "", MatchType.startsWith, matchFlags) for fieldName in searchableFields]
+                    return andOrExpression(allOf, matchList)
+                    '''
+
+
+            def andOrExpression(propFilterAllOf, matchList):
+                matchList = list(set(matchList))
+                if propFilterAllOf and len(matchList) > 1:
+                    # add OR expression because parent will AND
+                    return [CompoundExpression(matchList, Operand.OR), ]
+                else:
+                    return matchList
+
+
+            def paramFilterElementExpression(propFilterAllOf, paramFilterElement): #@UnusedVariable
+
+                params = vCardPropToParamMap.get(propFilter.filter_name.upper())
+                defined = params and paramFilterElement.filter_name.upper() in params
+
+                #defined test
+                if defined != paramFilterElement.defined:
+                    return False
+
+                #parameter value text match
+                if defined and paramFilterElement.filters:
+                    paramValues = params[paramFilterElement.filter_name.upper()]
+                    if paramValues and paramFilterElement.filters[0].text.upper() not in paramValues:
+                        return False
+
+                return True
+
+
+            def textMatchElementExpression(propFilterAllOf, textMatchElement):
+
+                # pre process text match strings for ds query
+                def getMatchStrings(propFilter, matchString):
+
+                    if propFilter.filter_name in ("REV" , "BDAY",):
+                        rawString = matchString
+                        matchString = ""
+                        for c in rawString:
+                            if not c in "TZ-:":
+                                matchString += c
+                    elif propFilter.filter_name == "GEO":
+                        matchString = ",".join(matchString.split(";"))
+
+                    if propFilter.filter_name in ("N" , "ADR", "ORG",):
+                        # for structured properties, change into multiple strings for ds query
+                        if propFilter.filter_name == "ADR":
+                            #split by newline and comma
+                            rawStrings = ",".join(matchString.split("\n")).split(",")
+                        else:
+                            #split by space
+                            rawStrings = matchString.split(" ")
+
+                        # remove empty strings
+                        matchStrings = []
+                        for oneString in rawStrings:
+                            if len(oneString):
+                                matchStrings += [oneString, ]
+                        return matchStrings
+
+                    elif len(matchString):
+                        return [matchString, ]
+                    else:
+                        return []
+                    # end getMatchStrings
+
+                if constant:
+                    #FIXME: match is not implemented in twisteddaldav.query.Filter.TextMatch so use _match for now
+                    return textMatchElement._match([constant, ])
+                else:
+
+                    matchStrings = getMatchStrings(propFilter, textMatchElement.text)
+
+                    if not len(matchStrings):
+                        # no searching text in binary ds attributes, so change to defined/not defined case
+                        if textMatchElement.negate:
+                            return definedExpression(False, propFilterAllOf)
+                        # else fall through to attribute exists case below
+                    else:
+
+                        # use match_type where possible depending on property/attribute mapping
+                        # FIXME: case-sensitive negate will not work.  This should return all all records in that case
+                        matchType = MatchType.contains
+                        if propFilter.filter_name in ("NICKNAME" , "TITLE" , "NOTE" , "UID", "URL", "N", "ADR", "ORG", "REV", "LABEL",):
+                            if textMatchElement.match_type == "equals":
+                                matchType = MatchType.equals
+                            elif textMatchElement.match_type == "starts-with":
+                                matchType = MatchType.startsWith
+                            elif textMatchElement.match_type == "ends-with":
+                                matchType = MatchType.endsWith
+
+                        matchList = []
+                        for matchString in matchStrings:
+                            matchFlags = None
+                            if textMatchElement.collation == "i;unicode-casemap" and textMatchElement.negate:
+                                matchFlags = MatchFlags.caseInsensitive | MatchFlags.NOT
+                            elif textMatchElement.collation == "i;unicode-casemap":
+                                matchFlags = MatchFlags.caseInsensitive
+                            elif textMatchElement.negate:
+                                matchFlags = MatchFlags.NOT
+                            else:
+                                matchFlags = MatchFlags_none
+
+                            matchList = [matchExpression(fieldName, matchString, matchType, matchFlags) for fieldName in searchableFields]
+                            matchList.extend(matchList)
+                        return andOrExpression(propFilterAllOf, matchList)
+
+                # attribute exists search
+                return definedExpression(True, propFilterAllOf)
+                #end textMatchElementExpression()
+
+            # searchablePropFilterAttrNames are attributes to be used by this propfilter's expression
+            searchableFields = vcardPropToSearchableFieldMap.get(propFilter.filter_name, [])
+            if isinstance(searchableFields, NamedConstant):
+                searchableFields = (searchableFields,)
+
+            constant = constantProperties.get(propFilter.filter_name)
+            if not searchableFields and not constant:
+                # not allAttrNames means propFilter.filter_name is not mapped
+                # return None to try to match all items if this is the only property filter
+                return None
+
+            #create a textMatchElement for the IsNotDefined qualifier
+            if isinstance(propFilter.qualifier, IsNotDefined):
+                textMatchElement = TextMatch(carddavxml.TextMatch.fromString(""))
+                textMatchElement.negate = True
+                propFilter.filters.append(textMatchElement)
+
+            # if only one propFilter, then use filterAllOf as propFilterAllOf to reduce subexpressions and simplify generated query string
+            if len(propFilter.filters) == 1:
+                propFilterAllOf = filterAllOf
             else:
-                updateLock = self.directory.updateLock()
-                yield updateLock.acquire()
-                try:
-                    response = (yield maybeDeferred(super(DirectoryBackedAddressBookResource, self).renderHTTP, request))
+                propFilterAllOf = propFilter.propfilter_test == "allof"
 
-                finally:
-                    yield updateLock.release()
+            propFilterExpressions = None
+            for propFilterElement in propFilter.filters:
+                propFilterExpression = None
+                if isinstance(propFilterElement, ParameterFilter):
+                    propFilterExpression = paramFilterElementExpression(propFilterAllOf, propFilterElement)
+                elif isinstance(propFilterElement, TextMatch):
+                    propFilterExpression = textMatchElementExpression(propFilterAllOf, propFilterElement)
+                propFilterExpressions = combineExpressionLists(propFilterExpressions, propFilterAllOf, propFilterExpression)
+                if isinstance(propFilterExpressions, bool) and propFilterAllOf != propFilterExpression:
+                    break
 
-                returnValue(response)
+            if isinstance(propFilterExpressions, list):
+                propFilterExpressions = list(set(propFilterExpressions))
+                if propFilterExpressions and (filterAllOf != propFilterAllOf):
+                    propFilterExpressions = [CompoundExpression(propFilterExpressions, Operand.AND if propFilterAllOf else Operand.OR)]
+
+            return propFilterExpressions
+            #end propFilterExpression
+
+        expressions = None
+        for propFilter in propFilters:
+
+            propExpressions = propFilterExpression(filterAllOf, propFilter)
+            expressions = combineExpressionLists(expressions, filterAllOf, propExpressions)
+
+            # early loop exit
+            if isinstance(expressions, bool) and filterAllOf != expressions:
+                break
+
+        # convert to needsAllRecords to return
+        # log.debug("expressionFromABFilter: expressions={q!r}", q=expressions,)
+        if isinstance(expressions, list):
+            expressions = list(set(expressions))
+            if len(expressions) > 1:
+                expr = CompoundExpression(expressions, Operand.AND if filterAllOf else Operand.OR)
+            elif len(expressions):
+                expr = expressions[0]
+            else:
+                expr = not filterAllOf  # empty expression list. should not happen
+        elif expressions is None:
+            expr = not filterAllOf
+        else:
+            # True or False
+            expr = expressions
+
+        properties = [propFilter.filter_name for propFilter in propFilters]
+
+        return (tuple(set(properties)), expr)
+
+    # Top-level filter contains zero or more prop-filters
+    properties = tuple()
+    expression = None
+    if addressBookFilter:
+        filterAllOf = addressBookFilter.filter_test == "allof"
+        if len(addressBookFilter.children):
+            properties, expression = propFilterListQuery(filterAllOf, addressBookFilter.children)
+        else:
+            expression = not filterAllOf
+
+    #log.debug("expressionFromABFilter: expression={q!r}, properties={pn}", q=expression, pn=properties)
+    return((properties, expression))
+
+
+
+class ABDirectoryQueryResult(DAVPropertyMixIn):
+    """
+    Result from ab query report or multiget on directory
+    """
+
+    def __init__(self, directoryBackedAddressBook,):
+
+        self._directoryBackedAddressBook = directoryBackedAddressBook
+        #self._vCard = None
+
+
+    def __repr__(self):
+        return "<{self.__class__.__name__}[{rn}({uid})]>".format(
+            self=self,
+            fn=self.vCard().propertyValue("FN"),
+            uid=self.vCard().propertyValue("UID")
+        )
+
+    '''
+    def __hash__(self):
+        s = "".join([
+              "{attr}:{values}".format(attr=attribute, values=self.valuesForAttribute(attribute),)
+              for attribute in self.attributes
+              ])
+        return hash(s)
+    '''
+
+    @inlineCallbacks
+    def generate(self, record, forceKind=None, addProps=None,):
+        self._vCard = yield vCardFromRecord(record, forceKind, addProps, None)
+        returnValue(self)
+
+
+    def vCard(self):
+        return self._vCard
+
+
+    def vCardText(self):
+        return str(self._vCard)
+
+
+    def uri(self):
+        return self.vCard().propertyValue("UID") + ".vcf"
+
+
+    def hRef(self, parentURI=None):
+        return davxml.HRef.fromString(joinURL(parentURI if parentURI else  self._directoryBackedAddressBook.uri, self.uri()))
+
+
+    def readProperty(self, property, request):
+
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+        namespace, name = qname
+
+        if namespace == dav_namespace:
+            if name == "resourcetype":
+                result = davxml.ResourceType.empty #@UndefinedVariable
+                return result
+            elif name == "getetag":
+                result = davxml.GETETag(ETag(hashlib.md5(self.vCardText()).hexdigest()).generate())
+                return result
+            elif name == "getcontenttype":
+                mimeType = MimeType('text', 'vcard', {})
+                result = davxml.GETContentType(generateContentType(mimeType))
+                return result
+            elif name == "getcontentlength":
+                result = davxml.GETContentLength.fromString(str(len(self.vCardText())))
+                return result
+            elif name == "getlastmodified":
+                if self.vCard().hasProperty("REV"):
+                    modDatetime = parse_date(self.vCard().propertyValue("REV"))
+                else:
+                    modDatetime = datetime.datetime.utcnow()
+
+                #strip time zone because time zones are unimplemented in davxml.GETLastModified.fromDate
+                d = modDatetime.date()
+                t = modDatetime.time()
+                modDatetimeNoTZ = datetime.datetime(d.year, d.month, d.day, t.hour, t.minute, t.second, t.microsecond, None)
+                result = davxml.GETLastModified.fromDate(modDatetimeNoTZ)
+                return result
+            elif name == "creationdate":
+                if self.vCard().hasProperty("REV"):  # use modification date property if it exists
+                    creationDatetime = parse_date(self.vCard().propertyValue("REV"))
+                else:
+                    creationDatetime = datetime.datetime.utcnow()
+                result = davxml.CreationDate.fromDate(creationDatetime)
+                return result
+            elif name == "displayname":
+                # AddressBook.app uses N. Use FN or UID instead?
+                result = davxml.DisplayName.fromString(self.vCard().propertyValue("N"))
+                return result
+
+        elif namespace == twisted_dav_namespace:
+            return super(ABDirectoryQueryResult, self).readProperty(property, request)
+
+        return self._directoryBackedAddressBook.readProperty(property, request)
+
+
+    def listProperties(self, request):  # @UnusedVariable
+        qnames = set(self.liveProperties())
+
+        # Add dynamic live properties that exist
+        dynamicLiveProperties = (
+            (dav_namespace, "quota-available-bytes"),
+            (dav_namespace, "quota-used-bytes"),
+        )
+        for dqname in dynamicLiveProperties:
+            qnames.remove(dqname)
+
+        for qname in self.deadProperties().list():
+            if (qname not in qnames) and (qname[0] != twisted_private_namespace):
+                qnames.add(qname)
+
+        yield qnames
+
+    listProperties = deferredGenerator(listProperties)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/extensions.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/extensions.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -66,8 +66,8 @@
 from twistedcaldav.method.report import http_REPORT
 
 from twistedcaldav.config import config
+from twext.who.expression import Operand, MatchType, MatchFlags
 
-
 thisModule = getModule(__name__)
 
 log = Logger()
@@ -95,7 +95,7 @@
             msg = "Bad XML: unknown value for test attribute: %s" % (testMode,)
             log.warn(msg)
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
-        operand = "and" if testMode == "allof" else "or"
+        operand = Operand.AND if testMode == "allof" else Operand.OR
 
         # Are we narrowing results down to a single CUTYPE?
         cuType = principal_property_search.attributes.get("type", None)
@@ -144,10 +144,18 @@
                     log.warn(msg)
                     raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
 
+                # Convert to twext.who.expression form
+                matchType = {
+                    "starts-with": MatchType.startsWith,
+                    "contains": MatchType.contains,
+                    "equals": MatchType.equals
+                }.get(matchType)
+                matchFlags = MatchFlags.caseInsensitive if caseless else MatchFlags.none
+
                 # Ignore any query strings under three letters
-                matchText = str(match)
+                matchText = match.toString()  # gives us unicode
                 if len(matchText) >= 3:
-                    propertySearches.append((props.children, matchText, caseless, matchType))
+                    propertySearches.append((props.children, matchText, matchFlags, matchType))
 
             elif child.qname() == (calendarserver_namespace, "limit"):
                 try:
@@ -182,7 +190,7 @@
         # See if we can take advantage of the directory
         fields = []
         nonDirectorySearches = []
-        for props, match, caseless, matchType in propertySearches:
+        for props, match, matchFlags, matchType in propertySearches:
             nonDirectoryProps = []
             for prop in props:
                 try:
@@ -191,12 +199,12 @@
                 except ValueError, e:
                     raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
                 if fieldName:
-                    fields.append((fieldName, match, caseless, matchType))
+                    fields.append((fieldName, match, matchFlags, matchType))
                 else:
                     nonDirectoryProps.append(prop)
             if nonDirectoryProps:
                 nonDirectorySearches.append((nonDirectoryProps, match,
-                    caseless, matchType))
+                    matchFlags, matchType))
 
         matchingResources = []
         matchcount = 0
@@ -208,7 +216,7 @@
                 operand=operand, cuType=cuType))
 
             for record in records:
-                resource = principalCollection.principalForRecord(record)
+                resource = yield principalCollection.principalForRecord(record)
                 if resource:
                     matchingResources.append(resource)
 
@@ -299,7 +307,7 @@
         records = (yield dir.recordsMatchingTokens(tokens, context=context))
 
         for record in records:
-            resource = principalCollection.principalForRecord(record)
+            resource = yield principalCollection.principalForRecord(record)
             if resource:
                 matchingResources.append(resource)
 
@@ -420,9 +428,9 @@
                 f.trap(HTTPError)
                 code = f.value.response.code
                 if code == responsecode.NOT_FOUND:
-                    log.error("Property %s was returned by listProperties() "
-                              "but does not exist for resource %s."
-                              % (name, self.resource))
+                    log.error("Property {p} was returned by listProperties() "
+                              "but does not exist for resource {r}.",
+                              p=name, r=self.resource)
                     return (name, None)
                 if code == responsecode.UNAUTHORIZED:
                     return (name, accessDeniedValue)
@@ -721,7 +729,13 @@
 
             elif name == "record-type":
                 if hasattr(self, "record"):
-                    returnValue(customxml.RecordType(self.record.recordType))
+                    returnValue(
+                        customxml.RecordType(
+                            self.record.service.recordTypeToOldName(
+                                self.record.recordType
+                            )
+                        )
+                    )
                 else:
                     raise HTTPError(StatusResponse(
                         responsecode.NOT_FOUND,
@@ -848,7 +862,7 @@
     ):
         # Permissions here are fixed, and are not subject to
         # inheritance rules, etc.
-        return succeed(self.defaultAccessControlList())
+        return self.defaultAccessControlList()
 
 
 
@@ -990,7 +1004,7 @@
             applyTo = True
 
         elif child.qname() == (calendarserver_namespace, "search-token"):
-            tokens.append(str(child))
+            tokens.append(child.toString())
 
         elif child.qname() == (calendarserver_namespace, "limit"):
             try:

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/freebusyurl.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/freebusyurl.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -102,7 +102,7 @@
                     davxml.Protected(),
                 ),
             )
-        return davxml.ACL(*aces)
+        return succeed(davxml.ACL(*aces))
 
 
     def resourceType(self):
@@ -243,7 +243,7 @@
         # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)
 
         # Now lookup the principal details for the targeted user
-        principal = self.parent.principalForRecord()
+        principal = (yield self.parent.principalForRecord())
 
         # Pick the first mailto cu address or the first other type
         cuaddr = None

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/ical.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/ical.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -34,6 +34,7 @@
 import itertools
 import uuid
 
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twext.python.log import Logger
 from txweb2.stream import IStream
 from txweb2.dav.util import allDataFromStream
@@ -3239,7 +3240,8 @@
                         self.removeProperty(attachment)
 
 
-    def normalizeCalendarUserAddresses(self, lookupFunction, principalFunction,
+    @inlineCallbacks
+    def normalizeCalendarUserAddresses(self, lookupFunction, recordFunction,
         toUUID=True):
         """
         Do the ORGANIZER/ATTENDEE property normalization.
@@ -3247,6 +3249,7 @@
         @param lookupFunction: function returning full name, guid, CUAs for a given CUA
         @type lookupFunction: L{Function}
         """
+
         for component in self.subcomponents():
             if component.name() in ignoredComponents:
                 continue
@@ -3259,7 +3262,7 @@
                 # Check that we can lookup this calendar user address - if not
                 # we cannot do anything with it
                 cuaddr = normalizeCUAddr(prop.value())
-                name, guid, cuaddrs = lookupFunction(cuaddr, principalFunction, config)
+                name, guid, cuaddrs = yield lookupFunction(cuaddr, recordFunction, config)
                 if guid is None:
                     continue
 
@@ -3275,7 +3278,9 @@
 
                 if toUUID:
                     # Always re-write value to urn:uuid
-                    prop.setValue("urn:uuid:%s" % (guid,))
+                    if isinstance(guid, uuid.UUID):
+                        guid = unicode(guid).upper()
+                    prop.setValue("urn:uuid:{guid}".format(guid=guid))
 
                 # If it is already a non-UUID address leave it be
                 elif cuaddr.startswith("urn:uuid:"):
@@ -3353,7 +3358,7 @@
 
             # For VPOLL also do immediate children
             if component.name() == "VPOLL":
-                component.normalizeCalendarUserAddresses(lookupFunction, principalFunction, toUUID)
+                yield component.normalizeCalendarUserAddresses(lookupFunction, recordFunction, toUUID)
 
 
     def allPerUserUIDs(self):
@@ -3563,15 +3568,16 @@
 # Utilities
 # #
 
-def normalizeCUAddress(cuaddr, lookupFunction, principalFunction, toUUID=True):
+ at inlineCallbacks
+def normalizeCUAddress(cuaddr, lookupFunction, recordFunction, toUUID=True):
     # Check that we can lookup this calendar user address - if not
     # we cannot do anything with it
-    _ignore_name, guid, cuaddrs = lookupFunction(normalizeCUAddr(cuaddr), principalFunction, config)
+    _ignore_name, guid, cuaddrs = (yield lookupFunction(normalizeCUAddr(cuaddr), recordFunction, config))
 
     if toUUID:
         # Always re-write value to urn:uuid
         if guid:
-            return "urn:uuid:%s" % (guid,)
+            returnValue("urn:uuid:%s" % (guid,))
 
     # If it is already a non-UUID address leave it be
     elif cuaddr.startswith("urn:uuid:"):
@@ -3610,9 +3616,9 @@
 
         # Make the change
         if newaddr:
-            return newaddr
+            returnValue(newaddr)
 
-    return cuaddr
+    returnValue(cuaddr)
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -43,6 +43,7 @@
 
 log = Logger()
 
+
 @inlineCallbacks
 def http_REPORT(self, request):
     """
@@ -58,7 +59,7 @@
     try:
         doc = (yield davXMLFromStream(request.stream))
     except ValueError, e:
-        log.error("Error while handling REPORT body: %s" % (e,))
+        log.error("Error while handling REPORT body: {err}", err=str(e))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
 
     if doc is None:
@@ -116,8 +117,8 @@
         #
         # Requested report is not supported.
         #
-        log.error("Unsupported REPORT %s for resource %s (no method %s)"
-                  % (encodeXMLName(namespace, name), self, method_name))
+        log.error("Unsupported REPORT {name} for resource {resource} (no method {method})",
+                  name=encodeXMLName(namespace, name), resource=self, method=method_name)
 
         raise HTTPError(ErrorResponse(
             responsecode.FORBIDDEN,

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_addressbook_query.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_addressbook_query.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_addressbook_query.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -21,26 +21,22 @@
 
 __all__ = ["report_urn_ietf_params_xml_ns_carddav_addressbook_query"]
 
-import urllib
-
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
-
 from twext.python.log import Logger
-from txweb2 import responsecode
-from txweb2.dav.http import ErrorResponse, MultiStatusResponse
-from txweb2.dav.method.report import NumberOfMatchesWithinLimits
-from txweb2.dav.util import joinURL
-from txweb2.http import HTTPError, StatusResponse
-
+from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred
 from twistedcaldav import carddavxml
-from twistedcaldav.config import config
 from twistedcaldav.carddavxml import carddav_namespace, NResults
+from twistedcaldav.config import config
 from twistedcaldav.method import report_common
-
 from txdav.carddav.datastore.query.filter import Filter
 from txdav.common.icommondatastore import ConcurrentModification, \
     IndexedSearchException
 from txdav.xml import element as davxml
+from txweb2 import responsecode
+from txweb2.dav.http import ErrorResponse, MultiStatusResponse
+from txweb2.dav.method.report import NumberOfMatchesWithinLimits
+from txweb2.dav.util import joinURL
+from txweb2.http import HTTPError, StatusResponse
+import urllib
 
 log = Logger()
 
@@ -52,12 +48,12 @@
     """
     # Verify root element
     if addressbook_query.qname() != (carddav_namespace, "addressbook-query"):
-        raise ValueError("{CardDAV:}addressbook-query expected as root element, not %s." % (addressbook_query.sname(),))
+        raise ValueError("{CardDAV:}addressbook-query expected as root element, not {elementName}.".format(elementName=addressbook_query.sname()))
 
     if not self.isCollection():
         parent = (yield self.locateParent(request, request.uri))
         if not parent.isAddressBookCollection():
-            log.error("addressbook-query report is not allowed on a resource outside of an address book collection %s" % (self,))
+            log.error("addressbook-query report is not allowed on a resource outside of an address book collection {parent}", parent=self)
             raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be address book collection or address book resource"))
 
     responses = []
@@ -154,160 +150,127 @@
                     # of one of these resources in another request.  In this
                     # case, we ignore the now missing resource rather
                     # than raise an error for the entire report.
-                    log.error("Missing resource during sync: %s" % (href,))
+                    log.error("Missing resource during sync: {href}", href=href)
 
 
         @inlineCallbacks
         def queryDirectoryBackedAddressBook(directoryBackedAddressBook, addressBookFilter):
             """
             """
-            records, limited[0] = (yield directoryBackedAddressBook.directory.vCardRecordsForAddressBookQuery(addressBookFilter, query, max_number_of_results[0]))
-            for vCardRecord in records:
+            results, limited[0] = yield directoryBackedAddressBook.doAddressBookDirectoryQuery(addressBookFilter, query, max_number_of_results[0], defaultKind="individual")
+            for vCardResult in results:
 
-                # match against original filter
-                if filter.match((yield vCardRecord.vCard())):
+                # match against original filter if different from addressBookFilter
+                if addressBookFilter is filter or filter.match((yield vCardResult.vCard())):
 
                     # Check size of results is within limit
                     checkMaxResults()
 
                     try:
-                        yield report_common.responseForHref(request, responses, vCardRecord.hRef(), vCardRecord, propertiesForResource, query, vcard=(yield vCardRecord.vCard()))
+                        yield report_common.responseForHref(request, responses, vCardResult.hRef(), vCardResult, propertiesForResource, query, vcard=(yield vCardResult.vCard()))
                     except ConcurrentModification:
                         # This can happen because of a race-condition between the
                         # time we determine which resources exist and the deletion
                         # of one of these resources in another request.  In this
                         # case, we ignore the now missing resource rather
                         # than raise an error for the entire report.
-                        log.error("Missing resource during sync: %s" % (vCardRecord.hRef(),))
+                        log.error("Missing resource during sync: {href}", href=vCardResult.hRef())
 
-        directoryAddressBookLock = None
-        try:
+        if not addrresource.isAddressBookCollection():
 
-            if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.cacheQuery:
+            #do UID lookup on last part of uri
+            resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
+            if resource_name.endswith(".vcf") and len(resource_name) > 4:
 
-                directory = addrresource.directory
-                if directory.liveQuery:
-                    # if liveQuery and cacheQuery, get vCards into the directory address book on disk
-                    directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery(filter, query, max_number_of_results[0]))
+                # see if parent is directory backed address book
+                parent = (yield  addrresource.locateParent(request, uri))
 
-                elif directory.maxDSQueryRecords and directory.maxDSQueryRecords < max_number_of_results[0]:
-                    max_number_of_results[0] = directory.maxDSQueryRecords
+        # Check whether supplied resource is an address book or an address book object resource
+        if addrresource.isAddressBookCollection():
 
-            elif not addrresource.isAddressBookCollection():
+            if addrresource.isDirectoryBackedAddressBookCollection():
+                yield  maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter)
 
-                #do UID lookup on last part of uri
-                resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
-                if resource_name.endswith(".vcf") and len(resource_name) > 4:
+            else:
 
-                    # see if parent is directory backed address book
-                    parent = (yield  addrresource.locateParent(request, uri))
+                # Do some optimisation of access control calculation by determining any inherited ACLs outside of
+                # the child resource loop and supply those to the checkPrivileges on each child.
+                filteredaces = (yield addrresource.inheritedACEsforChildren(request))
 
-                    if parent.isDirectoryBackedAddressBookCollection() and parent.directory.cacheQuery:
+                # Check for disabled access
+                if filteredaces is not None:
+                    index_query_ok = True
+                    try:
+                        # Get list of children that match the search and have read access
+                        names = [name for name, ignore_uid in (yield addrresource.search(filter))] #@UnusedVariable
+                    except IndexedSearchException:
+                        names = yield addrresource.listChildren()
+                        index_query_ok = False
+                    if not names:
+                        return
 
-                        directory = parent.directory
-                        if directory.liveQuery:
-                            vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
-                                                        carddavxml.TextMatch.fromString(resource_name[:-4]),
-                                                        name="UID", # attributes
-                                                        ), ])
-                            vCardFilter = Filter(vCardFilter)
+                    # Now determine which valid resources are readable and which are not
+                    ok_resources = []
+                    yield addrresource.findChildrenFaster(
+                        "1",
+                        request,
+                        lambda x, y: ok_resources.append((x, y)),
+                        None,
+                        None,
+                        None,
+                        names,
+                        (davxml.Read(),),
+                        inherited_aces=filteredaces
+                    )
+                    for child, child_uri in ok_resources:
+                        child_uri_name = child_uri[child_uri.rfind("/") + 1:]
 
-                            directoryAddressBookLock, limited[0] = (yield  directory.cacheVCardsForAddressBookQuery(vCardFilter, query, max_number_of_results[0]))
+                        if generate_address_data or not index_query_ok:
+                            vcard = yield child.vCard()
+                            assert vcard is not None, "vCard {name} is missing from address book collection {collection!r}".format(name=child_uri_name, collection=self)
+                        else:
+                            vcard = None
 
-                        elif directory.maxDSQueryRecords and directory.maxDSQueryRecords < max_number_of_results[0]:
-                            max_number_of_results[0] = directory.maxDSQueryRecords
+                        yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok=index_query_ok)
 
-            # Check whether supplied resource is an address book or an address book object resource
-            if addrresource.isAddressBookCollection():
+        else:
 
-                if addrresource.isDirectoryBackedAddressBookCollection() and addrresource.directory.liveQuery and not addrresource.directory.cacheQuery:
-                    yield  maybeDeferred(queryDirectoryBackedAddressBook, addrresource, filter)
+            handled = False
+            resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
+            if resource_name.endswith(".vcf") and len(resource_name) > 4:
 
-                else:
+                # see if parent is directory backed address book
+                parent = (yield  addrresource.locateParent(request, uri))
 
-                    # Do some optimisation of access control calculation by determining any inherited ACLs outside of
-                    # the child resource loop and supply those to the checkPrivileges on each child.
-                    filteredaces = (yield addrresource.inheritedACEsforChildren(request))
+                if parent.isDirectoryBackedAddressBookCollection():
 
-                    # Check for disabled access
-                    if filteredaces is not None:
-                        index_query_ok = True
-                        try:
-                            # Get list of children that match the search and have read access
-                            names = [name for name, ignore_uid in (yield addrresource.search(filter))] #@UnusedVariable
-                        except IndexedSearchException:
-                            names = yield addrresource.listChildren()
-                            index_query_ok = False
-                        if not names:
-                            return
+                    vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
+                                                carddavxml.TextMatch.fromString(resource_name[:-4]),
+                                                name="UID", # attributes
+                                                ), ])
+                    vCardFilter = Filter(vCardFilter)
 
-                        # Now determine which valid resources are readable and which are not
-                        ok_resources = []
-                        yield addrresource.findChildrenFaster(
-                            "1",
-                            request,
-                            lambda x, y: ok_resources.append((x, y)),
-                            None,
-                            None,
-                            None,
-                            names,
-                            (davxml.Read(),),
-                            inherited_aces=filteredaces
-                        )
-                        for child, child_uri in ok_resources:
-                            child_uri_name = child_uri[child_uri.rfind("/") + 1:]
+                    yield  maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
+                    handled = True
 
-                            if generate_address_data or not index_query_ok:
-                                vcard = yield child.vCard()
-                                assert vcard is not None, "vCard %s is missing from address book collection %r" % (child_uri_name, self)
-                            else:
-                                vcard = None
+            if not handled:
+                vcard = yield addrresource.vCard()
+                yield queryAddressBookObjectResource(addrresource, uri, None, vcard)
 
-                            yield queryAddressBookObjectResource(child, uri, child_uri_name, vcard, query_ok=index_query_ok)
+        if limited[0]:
+            raise NumberOfMatchesWithinLimits(matchcount[0])
 
-            else:
-
-                handled = False
-                resource_name = urllib.unquote(uri[uri.rfind("/") + 1:])
-                if resource_name.endswith(".vcf") and len(resource_name) > 4:
-
-                    # see if parent is directory backed address book
-                    parent = (yield  addrresource.locateParent(request, uri))
-
-                    if parent.isDirectoryBackedAddressBookCollection() and parent.directory.liveQuery and not parent.directory.cacheQuery:
-
-                        vCardFilter = carddavxml.Filter(*[carddavxml.PropertyFilter(
-                                                    carddavxml.TextMatch.fromString(resource_name[:-4]),
-                                                    name="UID", # attributes
-                                                    ), ])
-                        vCardFilter = Filter(vCardFilter)
-
-                        yield  maybeDeferred(queryDirectoryBackedAddressBook, parent, vCardFilter)
-                        handled = True
-
-                if not handled:
-                    vcard = yield addrresource.vCard()
-                    yield queryAddressBookObjectResource(addrresource, uri, None, vcard)
-
-            if limited[0]:
-                raise NumberOfMatchesWithinLimits(matchcount[0])
-
-        finally:
-            if directoryAddressBookLock:
-                yield directoryAddressBookLock.release()
-
     # Run report taking depth into account
     try:
         depth = request.headers.getHeader("depth", "0")
         yield report_common.applyToAddressBookCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),))
     except NumberOfMatchesWithinLimits, e:
-        self.log.info("Too many matching components in addressbook-query report. Limited to %d items" % e.maxLimit())
+        self.log.info("Too many matching components in addressbook-query report. Limited to {limit} items", limit=e.maxLimit())
         responses.append(davxml.StatusResponse(
                         davxml.HRef.fromString(request.uri),
                         davxml.Status.fromResponseCode(responsecode.INSUFFICIENT_STORAGE_SPACE),
                         davxml.Error(davxml.NumberOfMatchesWithinLimits()),
-                        #davxml.ResponseDescription("Results limited by %s at %d" % resultsWereLimited),
-                        davxml.ResponseDescription("Results limited to %d items" % e.maxLimit()),
+                        davxml.ResponseDescription("Results limited to {limit} items".format(limit=e.maxLimit())),
                     ))
 
     if not hasattr(request, "extendedLogItems"):

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_common.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_common.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -503,7 +503,7 @@
             returnValue(matchtotal)
 
     # May need organizer principal
-    organizer_principal = calresource.principalForCalendarUserAddress(organizer) if organizer else None
+    organizer_principal = (yield calresource.principalForCalendarUserAddress(organizer)) if organizer else None
     organizer_uid = organizer_principal.principalUID() if organizer_principal else ""
 
     # Free busy is per-user
@@ -630,7 +630,7 @@
                 if excludeuid:
                     # See if we have a UID match
                     if (excludeuid == uid):
-                        test_principal = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
+                        test_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
                         test_uid = test_principal.principalUID() if test_principal else ""
 
                         # Check that ORGANIZER's match (security requirement)
@@ -688,7 +688,7 @@
                 # See if we have a UID match
                 if (excludeuid == uid):
                     test_organizer = calendar.getOrganizer()
-                    test_principal = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
+                    test_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
                     test_uid = test_principal.principalUID() if test_principal else ""
 
                     # Check that ORGANIZER's match (security requirement)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_multiget_common.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_multiget_common.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/method/report_multiget_common.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,17 +20,8 @@
 
 __all__ = ["multiget_common"]
 
-from urllib import unquote
-
 from twext.python.log import Logger
-
-from txweb2 import responsecode
-from txweb2.dav.http import ErrorResponse, MultiStatusResponse
-from txweb2.dav.resource import AccessDeniedError
-from txweb2.http import HTTPError, StatusResponse
-
 from twisted.internet.defer import inlineCallbacks, returnValue
-
 from twistedcaldav import carddavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.carddavxml import carddav_namespace
@@ -38,11 +29,15 @@
 from twistedcaldav.method import report_common
 from twistedcaldav.method.report_common import COLLECTION_TYPE_CALENDAR, \
     COLLECTION_TYPE_ADDRESSBOOK
-
 from txdav.carddav.datastore.query.filter import Filter
 from txdav.common.icommondatastore import ConcurrentModification
 from txdav.xml import element as davxml
 from txdav.xml.base import dav_namespace
+from txweb2 import responsecode
+from txweb2.dav.http import ErrorResponse, MultiStatusResponse
+from txweb2.dav.resource import AccessDeniedError
+from txweb2.http import HTTPError, StatusResponse
+from urllib import unquote
 
 log = Logger()
 
@@ -58,11 +53,11 @@
 
         if collection_type == COLLECTION_TYPE_CALENDAR:
             if not parent.isPseudoCalendarCollection():
-                log.error("calendar-multiget report is not allowed on a resource outside of a calendar collection %s" % (self,))
+                log.error("calendar-multiget report is not allowed on a resource outside of a calendar collection {res}", res=self)
                 raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be calendar resource"))
         elif collection_type == COLLECTION_TYPE_ADDRESSBOOK:
             if not parent.isAddressBookCollection():
-                log.error("addressbook-multiget report is not allowed on a resource outside of an address book collection %s" % (self,))
+                log.error("addressbook-multiget report is not allowed on a resource outside of an address book collection {res}", res=self)
                 raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be address book resource"))
 
     responses = []
@@ -106,7 +101,7 @@
 
     # Check size of results is within limit when data property requested
     if hasData and len(resources) > config.MaxMultigetWithDataHrefs:
-        log.error("Too many results in multiget report returning data: %d" % len(resources))
+        log.error("Too many resources in multiget report: {count}", count=len(resources))
         raise HTTPError(ErrorResponse(
             responsecode.FORBIDDEN,
             davxml.NumberOfMatchesWithinLimits(),
@@ -172,7 +167,7 @@
 
             # Special for addressbooks
             if collection_type == COLLECTION_TYPE_ADDRESSBOOK:
-                if self.isDirectoryBackedAddressBookCollection() and self.directory.liveQuery:
+                if self.isDirectoryBackedAddressBookCollection():
                     result = (yield doDirectoryAddressBookResponse())
                     returnValue(result)
 
@@ -214,8 +209,7 @@
                         isowner=isowner
                     )
                 except ValueError:
-                    log.error("Invalid calendar resource during multiget: %s" %
-                              (href,))
+                    log.error("Invalid calendar resource during multiget: {href}", href=href)
                     responses.append(davxml.StatusResponse(
                         davxml.HRef.fromString(href),
                         davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
@@ -225,7 +219,7 @@
                     # of one of these resources in another request.  In this
                     # case, return a 404 for the now missing resource rather
                     # than raise an error for the entire report.
-                    log.error("Missing resource during multiget: %s" % (href,))
+                    log.error("Missing resource during multiget: {href}", href=href)
                     responses.append(davxml.StatusResponse(
                         davxml.HRef.fromString(href),
                         davxml.Status.fromResponseCode(responsecode.NOT_FOUND)
@@ -255,11 +249,13 @@
                     resource_name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
                     if self._isChildURI(request, resource_uri) and resource_name.endswith(".vcf") and len(resource_name) > 4:
                         valid_hrefs.append(href)
+                        textMatchElement = carddavxml.TextMatch.fromString(resource_name[:-4])
+                        textMatchElement.attributes["match-type"] = "equals" # do equals compare. Default is "contains"
                         vCardFilters.append(carddavxml.PropertyFilter(
-                                                carddavxml.TextMatch.fromString(resource_name[:-4]),
+                                                textMatchElement,
                                                 name="UID", # attributes
                                             ))
-                    elif not self.directory.cacheQuery:
+                    else:
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
 
                 # exit if not valid
@@ -268,40 +264,29 @@
 
                 addressBookFilter = carddavxml.Filter(*vCardFilters)
                 addressBookFilter = Filter(addressBookFilter)
-                if self.directory.cacheQuery:
-                    # add vcards to directory address book and run "normal case" below
-                    limit = config.DirectoryAddressBook.MaxQueryResults
-                    directoryAddressBookLock, limited = (yield  self.directory.cacheVCardsForAddressBookQuery(addressBookFilter, propertyreq, limit))
-                    if limited:
-                        log.error("Too many results in multiget report: %d" % len(resources))
-                        raise HTTPError(ErrorResponse(
-                            responsecode.FORBIDDEN,
-                            (dav_namespace, "number-of-matches-within-limits"),
-                            "Too many results",
-                        ))
-                else:
-                    #get vCards and filter
-                    limit = config.DirectoryAddressBook.MaxQueryResults
-                    vCardRecords, limited = (yield self.directory.vCardRecordsForAddressBookQuery(addressBookFilter, propertyreq, limit))
-                    if limited:
-                        log.error("Too many results in multiget report: %d" % len(resources))
-                        raise HTTPError(ErrorResponse(
-                            responsecode.FORBIDDEN,
-                            (dav_namespace, "number-of-matches-within-limits"),
-                            "Too many results",
-                        ))
 
-                    for href in valid_hrefs:
-                        matchingRecord = None
-                        for vCardRecord in vCardRecords:
-                            if href == vCardRecord.hRef(): # might need to compare urls instead - also case sens ok?
-                                matchingRecord = vCardRecord
-                                break
+                #get vCards and filter
+                limit = config.DirectoryAddressBook.MaxQueryResults
+                results, limited = (yield self.doAddressBookDirectoryQuery(addressBookFilter, propertyreq, limit))
+                if limited:
+                    log.error("Too many results in multiget report: {count}", count=len(resources))
+                    raise HTTPError(ErrorResponse(
+                        responsecode.FORBIDDEN,
+                        (dav_namespace, "number-of-matches-within-limits"),
+                        "Too many results",
+                    ))
 
-                        if matchingRecord:
-                            yield report_common.responseForHref(request, responses, href, matchingRecord, propertiesForResource, propertyreq, vcard=matchingRecord.vCard())
-                        else:
-                            responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
+                for href in valid_hrefs:
+                    matchingResource = None
+                    for vCardResource in results:
+                        if href == vCardResource.hRef(): # might need to compare urls instead - also case sens ok?
+                            matchingResource = vCardResource
+                            break
+
+                    if matchingResource:
+                        yield report_common.responseForHref(request, responses, href, matchingResource, propertiesForResource, propertyreq, vcard=matchingResource.vCard())
+                    else:
+                        responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
             finally:
                 if directoryAddressBookLock:
                     yield directoryAddressBookLock.release()

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -77,7 +77,7 @@
 from twistedcaldav.linkresource import LinkResource
 from calendarserver.push.notifier import getPubSubAPSConfiguration
 from twistedcaldav.sharing import SharedResourceMixin, SharedHomeMixin
-from twistedcaldav.util import normalizationLookup
+from txdav.caldav.datastore.util import normalizationLookup
 from twistedcaldav.vcard import Component as vComponent
 
 from txdav.common.icommondatastore import InternalDataStoreError, \
@@ -863,7 +863,8 @@
                 home = self._newStoreObject.parentCollection().ownerHome()
             else:
                 home = self._newStoreObject.ownerHome()
-            returnValue(element.HRef(self.principalForUID(home.uid()).principalURL()))
+            principal = (yield self.principalForUID(home.uid()))
+            returnValue(element.HRef(principal.principalURL()))
         else:
             parent = (yield self.locateParent(request, request.urlForResource(self)))
         if parent and isinstance(parent, CalDAVResource):
@@ -883,7 +884,7 @@
                 home = self._newStoreObject.parentCollection().ownerHome()
             else:
                 home = self._newStoreObject.ownerHome()
-            returnValue(self.principalForUID(home.uid()))
+            returnValue((yield self.principalForUID(home.uid())))
         else:
             parent = (yield self.locateParent(request, request.urlForResource(self)))
         if parent and isinstance(parent, CalDAVResource):
@@ -933,8 +934,8 @@
             return None
 
         if 'record' in dir(self):
-            if self.record.fullName:
-                return self.record.fullName
+            if self.record.fullNames:
+                return self.record.fullNames[0]
             elif self.record.shortNames:
                 return self.record.shortNames[0]
             else:
@@ -1070,25 +1071,30 @@
 
         @param ical: calendar object to normalize.
         @type ical: L{Component}
+        @return: L{Deferred}
         """
-        ical.normalizeCalendarUserAddresses(normalizationLookup,
-            self.principalForCalendarUserAddress)
+        return ical.normalizeCalendarUserAddresses(
+            normalizationLookup,
+            self.record.service.recordWithCalendarUserAddress
+        )
 
 
+    @inlineCallbacks
     def principalForCalendarUserAddress(self, address):
         for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
+            principal = (yield principalCollection.principalForCalendarUserAddress(address))
             if principal is not None:
-                return principal
-        return None
+                returnValue(principal)
+        returnValue(None)
 
 
+    @inlineCallbacks
     def principalForUID(self, principalUID):
         for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForUID(principalUID)
+            principal = (yield principalCollection.principalForUID(principalUID))
             if principal is not None:
-                return principal
-        return None
+                returnValue(principal)
+        returnValue(None)
 
 
     @inlineCallbacks
@@ -1868,13 +1874,13 @@
                     *[element.HRef(principal.principalURL()) for principal in results]
                 ))
 
-            elif name == "auto-schedule" and self.calendarsEnabled():
-                autoSchedule = self.getAutoSchedule()
-                returnValue(customxml.AutoSchedule("true" if autoSchedule else "false"))
+            # elif name == "auto-schedule" and self.calendarsEnabled():
+            #     autoSchedule = self.getAutoSchedule()
+            #     returnValue(customxml.AutoSchedule("true" if autoSchedule else "false"))
 
             elif name == "auto-schedule-mode" and self.calendarsEnabled():
-                autoScheduleMode = self.getAutoScheduleMode()
-                returnValue(customxml.AutoScheduleMode(autoScheduleMode if autoScheduleMode else "default"))
+                autoScheduleMode = yield self.getAutoScheduleMode()
+                returnValue(customxml.AutoScheduleMode(autoScheduleMode.description if autoScheduleMode else "default"))
 
         elif namespace == carddav_namespace and self.addressBooksEnabled():
             if name == "addressbook-home-set":
@@ -2302,20 +2308,23 @@
     # ACL
     ##
 
+    @inlineCallbacks
     def owner(self, request):
-        return succeed(element.HRef(self.principalForRecord().principalURL()))
+        principal = yield self.principalForRecord()
+        returnValue(element.HRef(principal.principalURL()))
 
 
     def ownerPrincipal(self, request):
-        return succeed(self.principalForRecord())
+        return self.principalForRecord()
 
 
     def resourceOwnerPrincipal(self, request):
-        return succeed(self.principalForRecord())
+        return self.principalForRecord()
 
 
+    @inlineCallbacks
     def defaultAccessControlList(self):
-        myPrincipal = self.principalForRecord()
+        myPrincipal = yield self.principalForRecord()
 
         # Server may be read only
         if config.EnableReadOnlyServer:
@@ -2342,12 +2351,12 @@
         # Give all access to config.AdminPrincipals
         aces += config.AdminACEs
 
-        return element.ACL(*aces)
+        returnValue(element.ACL(*aces))
 
 
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
         # Permissions here are fixed, and are not subject to inheritance rules, etc.
-        return succeed(self.defaultAccessControlList())
+        return self.defaultAccessControlList()
 
 
     def principalCollections(self):
@@ -2555,9 +2564,10 @@
         return config.Sharing.Enabled and config.Sharing.Calendars.Enabled and self.exists()
 
 
+    @inlineCallbacks
     def _otherPrincipalHomeURL(self, otherUID):
-        ownerPrincipal = self.principalForUID(otherUID)
-        return ownerPrincipal.calendarHomeURLs()[0]
+        ownerPrincipal = (yield self.principalForUID(otherUID))
+        returnValue(ownerPrincipal.calendarHomeURLs()[0])
 
 
     @inlineCallbacks
@@ -2584,8 +2594,9 @@
         return self._newStoreHome.hasCalendarResourceUIDSomewhereElse(uid, ok_object._newStoreObject, mode)
 
 
+    @inlineCallbacks
     def defaultAccessControlList(self):
-        myPrincipal = self.principalForRecord()
+        myPrincipal = yield self.principalForRecord()
 
         # Server may be read only
         if config.EnableReadOnlyServer:
@@ -2652,7 +2663,7 @@
                 ),
             )
 
-        return element.ACL(*aces)
+        returnValue(element.ACL(*aces))
 
 
     @inlineCallbacks
@@ -2808,9 +2819,10 @@
         return config.Sharing.Enabled and config.Sharing.AddressBooks.Enabled and self.exists()
 
 
+    @inlineCallbacks
     def _otherPrincipalHomeURL(self, otherUID):
-        ownerPrincipal = self.principalForUID(otherUID)
-        return ownerPrincipal.addressBookHomeURLs()[0]
+        ownerPrincipal = (yield self.principalForUID(otherUID))
+        returnValue(ownerPrincipal.addressBookHomeURLs()[0])
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/scheduling_store/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/scheduling_store/caldav/resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/scheduling_store/caldav/resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -373,12 +373,14 @@
         if config.Scheduling.CalDAV.OldDraftCompatibility:
             privs += (davxml.Privilege(caldavxml.Schedule()),)
 
-        return davxml.ACL(
-            # CalDAV:schedule-deliver for any authenticated user
-            davxml.ACE(
-                davxml.Principal(davxml.Authenticated()),
-                davxml.Grant(*privs),
-            ),
+        return succeed(
+            davxml.ACL(
+                # CalDAV:schedule-deliver for any authenticated user
+                davxml.ACE(
+                    davxml.Principal(davxml.Authenticated()),
+                    davxml.Grant(*privs),
+                ),
+            )
         )
 
 
@@ -426,7 +428,7 @@
             principalURL = str(authz_principal)
             if principalURL:
                 authz = (yield request.locateResource(principalURL))
-                self._associatedTransaction._authz_uid = authz.record.guid
+                self._associatedTransaction._authz_uid = authz.record.uid
 
         # Log extended item
         if not hasattr(request, "extendedLogItems"):
@@ -532,9 +534,10 @@
         return succeed(sendSchedulePrivilegeSet)
 
 
+    @inlineCallbacks
     def defaultAccessControlList(self):
         if config.EnableProxyPrincipals:
-            myPrincipal = self.parent.principalForRecord()
+            myPrincipal = yield self.parent.principalForRecord()
 
             privs = (
                 davxml.Privilege(caldavxml.ScheduleSend()),
@@ -542,16 +545,18 @@
             if config.Scheduling.CalDAV.OldDraftCompatibility:
                 privs += (davxml.Privilege(caldavxml.Schedule()),)
 
-            return davxml.ACL(
-                # CalDAV:schedule for associated write proxies
-                davxml.ACE(
-                    davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write"))),
-                    davxml.Grant(*privs),
-                    davxml.Protected(),
-                ),
+            returnValue(
+                davxml.ACL(
+                    # CalDAV:schedule for associated write proxies
+                    davxml.ACE(
+                        davxml.Principal(davxml.HRef(joinURL(myPrincipal.principalURL(), "calendar-proxy-write"))),
+                        davxml.Grant(*privs),
+                        davxml.Protected(),
+                    ),
+                )
             )
         else:
-            return super(ScheduleOutboxResource, self).defaultAccessControlList()
+            returnValue(super(ScheduleOutboxResource, self).defaultAccessControlList())
 
 
     def report_urn_ietf_params_xml_ns_caldav_calendar_query(self, request, calendar_query):

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/sharing.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/sharing.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -44,7 +44,7 @@
 from twistedcaldav import customxml, caldavxml
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.directory.wiki import WikiDirectoryService, getWikiAccess
+from txdav.who.wiki import RecordType as WikiRecordType, WikiAccessLevel
 from twistedcaldav.linkresource import LinkFollowerMixIn
 
 
@@ -62,24 +62,25 @@
         """
         if config.Sharing.Enabled:
 
+            @inlineCallbacks
             def invitePropertyElement(invitation, includeUID=True):
 
                 userid = "urn:uuid:" + invitation.shareeUID
-                principal = self.principalForUID(invitation.shareeUID)
+                principal = yield self.principalForUID(invitation.shareeUID)
                 cn = principal.displayName() if principal else invitation.shareeUID
-                return customxml.InviteUser(
+                returnValue(customxml.InviteUser(
                     customxml.UID.fromString(invitation.uid) if includeUID else None,
                     element.HRef.fromString(userid),
                     customxml.CommonName.fromString(cn),
                     customxml.InviteAccess(invitationBindModeToXMLMap[invitation.mode]()),
                     invitationBindStatusToXMLMap[invitation.status](),
-                )
+                ))
 
             # See if this property is on the shared calendar
             if self.isShared():
                 invitations = yield self.validateInvites(request)
                 returnValue(customxml.Invite(
-                    *[invitePropertyElement(invitation) for invitation in invitations]
+                    *[(yield invitePropertyElement(invitation)) for invitation in invitations]
                 ))
 
             # See if it is on the sharee calendar
@@ -89,7 +90,7 @@
                     invitations = yield original.allInvitations()
                     invitations = yield self.validateInvites(request, invitations)
 
-                    ownerPrincipal = self.principalForUID(self._newStoreObject.ownerHome().uid())
+                    ownerPrincipal = yield self.principalForUID(self._newStoreObject.ownerHome().uid())
                     # FIXME:  use urn:uuid in all cases
                     if self.isCalendarCollection():
                         owner = ownerPrincipal.principalURL()
@@ -102,7 +103,7 @@
                             element.HRef.fromString(owner),
                             customxml.CommonName.fromString(ownerCN),
                         ),
-                        *[invitePropertyElement(invitation, includeUID=False) for invitation in invitations]
+                        *[(yield invitePropertyElement(invitation, includeUID=False)) for invitation in invitations]
                     ))
 
         returnValue(None)
@@ -266,17 +267,15 @@
             any access at all.
         """
         if self._newStoreObject.direct():
-            owner = self.principalForUID(self._newStoreObject.ownerHome().uid())
-            sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
-            if owner.record.recordType == WikiDirectoryService.recordType_wikis:
+            owner = yield self.principalForUID(self._newStoreObject.ownerHome().uid())
+            sharee = yield self.principalForUID(self._newStoreObject.viewerHome().uid())
+            if owner.record.recordType == WikiRecordType.macOSXServerWiki:
                 # Access level comes from what the wiki has granted to the
                 # sharee
-                userID = sharee.record.guid
-                wikiID = owner.record.shortNames[0]
-                access = (yield getWikiAccess(userID, wikiID))
-                if access == "read":
+                access = (yield owner.record.accessForRecord(sharee.record))
+                if access == WikiAccessLevel.read:
                     returnValue("read-only")
-                elif access in ("write", "admin"):
+                elif access == WikiAccessLevel.write:
                     returnValue("read-write")
                 else:
                     returnValue(None)
@@ -320,7 +319,7 @@
         assert self._isShareeResource, "Only call this for a sharee resource"
         assert self.isCalendarCollection() or self.isAddressBookCollection(), "Only call this for a address book or calendar resource"
 
-        sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
+        sharee = yield self.principalForUID(self._newStoreObject.viewerHome().uid())
         access = yield self._checkAccessControl()
 
         if access == "original" and not self._newStoreObject.ownerHome().external():
@@ -411,7 +410,7 @@
         """
 
         # First try to resolve as a principal
-        principal = self.principalForCalendarUserAddress(userid)
+        principal = yield self.principalForCalendarUserAddress(userid)
         if principal:
             if request:
                 ownerPrincipal = (yield self.ownerPrincipal(request))
@@ -501,10 +500,10 @@
 
 
     @inlineCallbacks
-    def inviteSingleUserToShare(self, userid, cn, ace, summary, request): #@UnusedVariable
+    def inviteSingleUserToShare(self, userid, cn, ace, summary, request):  #@UnusedVariable
 
         # We currently only handle local users
-        sharee = self.principalForCalendarUserAddress(userid)
+        sharee = yield self.principalForCalendarUserAddress(userid)
         if not sharee:
             returnValue(False)
 
@@ -521,7 +520,7 @@
     def uninviteSingleUserFromShare(self, userid, aces, request): #@UnusedVariable
 
         # Cancel invites - we'll just use whatever userid we are given
-        sharee = self.principalForCalendarUserAddress(userid)
+        sharee = yield self.principalForCalendarUserAddress(userid)
         if not sharee:
             returnValue(False)
 
@@ -792,7 +791,7 @@
         Set shared state and check access control.
         """
         if child._newStoreObject is not None and not child._newStoreObject.owned():
-            ownerHomeURL = self._otherPrincipalHomeURL(child._newStoreObject.ownerHome().uid())
+            ownerHomeURL = (yield self._otherPrincipalHomeURL(child._newStoreObject.ownerHome().uid()))
             ownerView = yield child._newStoreObject.ownerView()
             child.setShare(joinURL(ownerHomeURL, ownerView.name()))
             access = yield child._checkAccessControl()
@@ -802,6 +801,7 @@
 
 
     def _otherPrincipalHomeURL(self, otherUID):
+        # Is this only meant to be overridden?
         pass
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/stdconfig.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/stdconfig.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -374,10 +374,13 @@
     #    users, groups, locations and resources) to the server.
     #
     "DirectoryService": {
+        "Enabled": True,
         "type": "twistedcaldav.directory.xmlfile.XMLDirectoryService",
         "params": DEFAULT_SERVICE_PARAMS["twistedcaldav.directory.xmlfile.XMLDirectoryService"],
     },
 
+    "DirectoryRealmName": "",
+
     #
     # Locations and Resources service
     #
@@ -385,7 +388,7 @@
     #    and resources.
     #
     "ResourceService": {
-        "Enabled" : True,
+        "Enabled": True,
         "type": "twistedcaldav.directory.xmlfile.XMLDirectoryService",
         "params": DEFAULT_RESOURCE_PARAMS["twistedcaldav.directory.xmlfile.XMLDirectoryService"],
     },
@@ -451,10 +454,6 @@
         "Wiki": {
             "Enabled": False,
             "Cookie": "cc.collabd_session_guid",
-            "URL": "http://127.0.0.1:8089/RPC2",
-            "UserMethod": "userForSession",
-            "WikiMethod": "accessLevelForUserWikiCalendar",
-            "LionCompatibility": False,
             "CollabHost": "localhost",
             "CollabPort": 4444,
         },
@@ -1016,8 +1015,6 @@
         "Enabled": True,
         "MemcachedPool" : "Default",
         "UpdateSeconds" : 300,
-        "ExpireSeconds" : 86400,
-        "LockSeconds"   : 600,
         "EnableUpdater" : True,
         "UseExternalProxies" : False,
     },
@@ -1254,8 +1251,18 @@
             hostname = "localhost"
         configDict.ServerHostName = hostname
 
+    # Default DirectoryRealmName from ServerHostName
+    if not configDict.DirectoryRealmName:
+        # Use system-wide realm on OSX
+        try:
+            import ServerFoundation
+            realmName = ServerFoundation.XSAuthenticator.defaultRealm()
+            configDict.DirectoryRealmName = realmName
+        except ImportError:
+            configDict.DirectoryRealmName = configDict.ServerHostName
 
 
+
 def _updateMultiProcess(configDict, reloading=False):
     """
     Dynamically compute ProcessCount if it's set to 0.  Always compute

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/storebridge.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/storebridge.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -15,77 +15,89 @@
 # limitations under the License.
 ##
 
+import collections
+import hashlib
+import time
+from urlparse import urlsplit, urljoin
+import uuid
+
 from pycalendar.datetime import DateTime
-
 from twext.enterprise.locking import LockTimeout
 from twext.python.log import Logger
-from txweb2 import responsecode, http_headers, http
-from txweb2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
-from txweb2.dav.noneprops import NonePropertyStore
-from txweb2.dav.resource import TwistedACLInheritable, AccessDeniedError, \
-    davPrivilegeSet
-from txweb2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
-from txweb2.filter.location import addLocation
-from txweb2.http import HTTPError, StatusResponse, Response
-from txweb2.http_headers import ETag, MimeType, MimeDisposition
-from txweb2.iweb import IResponse
-from txweb2.responsecode import \
-    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED, \
-    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
-from txweb2.stream import ProducerStream, readStream, MemoryStream
-
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue, maybeDeferred
 from twisted.internet.protocol import Protocol
 from twisted.python.hashlib import md5
 from twisted.python.util import FancyEqMixin
-
 from twistedcaldav import customxml, carddavxml, caldavxml, ical
-from twistedcaldav.caldavxml import caldav_namespace, MaxAttendeesPerInstance, \
-    MaxInstances, NoUIDConflict
+from twistedcaldav.caldavxml import (
+    caldav_namespace, MaxAttendeesPerInstance, MaxInstances, NoUIDConflict
+)
 from twistedcaldav.carddavxml import carddav_namespace, NoUIDConflict as NovCardUIDConflict
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.directory.wiki import WikiDirectoryService, getWikiAccess
-from twistedcaldav.ical import Component as VCalendar, Property as VProperty, \
-    InvalidICalendarDataError, iCalendarProductID, Component
-from twistedcaldav.instance import InvalidOverriddenInstanceError, \
-    TooManyInstancesError
+from twistedcaldav.ical import (
+    Component as VCalendar, Property as VProperty, InvalidICalendarDataError,
+    iCalendarProductID, Component
+)
+from twistedcaldav.instance import (
+    InvalidOverriddenInstanceError, TooManyInstancesError
+)
 from twistedcaldav.memcachelock import MemcacheLockTimeoutError
 from twistedcaldav.notifications import NotificationCollectionResource, NotificationResource
 from twistedcaldav.resource import CalDAVResource, DefaultAlarmPropertyMixin
 from twistedcaldav.scheduling_store.caldav.resource import ScheduleInboxResource
-from twistedcaldav.sharing import invitationBindStatusToXMLMap, \
-    invitationBindModeToXMLMap
+from twistedcaldav.sharing import (
+    invitationBindStatusToXMLMap, invitationBindModeToXMLMap
+)
 from twistedcaldav.util import bestAcceptType
 from twistedcaldav.vcard import Component as VCard, InvalidVCardDataError
-
 from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.icalendarstore import QuotaExceeded, AttachmentStoreFailed, \
-    AttachmentStoreValidManagedID, AttachmentRemoveFailed, \
-    AttachmentDropboxNotAllowed, InvalidComponentTypeError, \
-    TooManyAttendeesError, InvalidCalendarAccessError, ValidOrganizerError, \
-    InvalidPerUserDataMerge, \
-    AttendeeAllowedError, ResourceDeletedError, InvalidAttachmentOperation, \
+from txdav.caldav.icalendarstore import (
+    QuotaExceeded, AttachmentStoreFailed,
+    AttachmentStoreValidManagedID, AttachmentRemoveFailed,
+    AttachmentDropboxNotAllowed, InvalidComponentTypeError,
+    TooManyAttendeesError, InvalidCalendarAccessError, ValidOrganizerError,
+    InvalidPerUserDataMerge,
+    AttendeeAllowedError, ResourceDeletedError, InvalidAttachmentOperation,
     ShareeAllowedError, DuplicatePrivateCommentsError, InvalidSplit
-from txdav.carddav.iaddressbookstore import KindChangeNotAllowedError, \
-    GroupWithUnsharedAddressNotAllowedError
-from txdav.common.datastore.sql_tables import _BIND_MODE_READ, _BIND_MODE_WRITE, \
+)
+from txdav.carddav.iaddressbookstore import (
+    KindChangeNotAllowedError, GroupWithUnsharedAddressNotAllowedError
+)
+from txdav.common.datastore.sql_tables import (
+    _BIND_MODE_READ, _BIND_MODE_WRITE,
     _BIND_MODE_DIRECT, _BIND_STATUS_ACCEPTED
-from txdav.common.icommondatastore import NoSuchObjectResourceError, \
-    TooManyObjectResourcesError, ObjectResourceTooBigError, \
-    InvalidObjectResourceError, ObjectResourceNameNotAllowedError, \
-    ObjectResourceNameAlreadyExistsError, UIDExistsError, \
-    UIDExistsElsewhereError, InvalidUIDError, InvalidResourceMove, \
+)
+from txdav.common.icommondatastore import (
+    NoSuchObjectResourceError,
+    TooManyObjectResourcesError, ObjectResourceTooBigError,
+    InvalidObjectResourceError, ObjectResourceNameNotAllowedError,
+    ObjectResourceNameAlreadyExistsError, UIDExistsError,
+    UIDExistsElsewhereError, InvalidUIDError, InvalidResourceMove,
     InvalidComponentForStoreError
+)
 from txdav.idav import PropertyChangeNotAllowedError
+from txdav.who.wiki import RecordType as WikiRecordType
 from txdav.xml import element as davxml, element
 from txdav.xml.base import dav_namespace, WebDAVUnknownElement, encodeXMLName
+from txweb2 import responsecode, http_headers, http
+from txweb2.dav.http import ErrorResponse, ResponseQueue, MultiStatusResponse
+from txweb2.dav.noneprops import NonePropertyStore
+from txweb2.dav.resource import (
+    TwistedACLInheritable, AccessDeniedError, davPrivilegeSet
+)
+from txweb2.dav.util import parentForURL, allDataFromStream, joinURL, davXMLFromStream
+from txweb2.filter.location import addLocation
+from txweb2.http import HTTPError, StatusResponse, Response
+from txweb2.http_headers import ETag, MimeType, MimeDisposition
+from txweb2.iweb import IResponse
+from txweb2.responsecode import (
+    FORBIDDEN, NO_CONTENT, NOT_FOUND, CREATED, CONFLICT, PRECONDITION_FAILED,
+    BAD_REQUEST, OK, INSUFFICIENT_STORAGE_SPACE, SERVICE_UNAVAILABLE
+)
+from txweb2.stream import ProducerStream, readStream, MemoryStream
 
-from urlparse import urlsplit, urljoin
-import collections
-import hashlib
-import time
-import uuid
+
 """
 Wrappers to translate between the APIs in L{txdav.caldav.icalendarstore} and
 L{txdav.carddav.iaddressbookstore} and those in L{twistedcaldav}.
@@ -93,6 +105,7 @@
 
 log = Logger()
 
+
 class _NewStorePropertiesWrapper(object):
     """
     Wrap a new-style property store (a L{txdav.idav.IPropertyStore}) in the old-
@@ -1600,7 +1613,7 @@
         cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
         newACEs = []
         for calendarUserAddress in cuas:
-            principal = self.principalForCalendarUserAddress(
+            principal = yield self.principalForCalendarUserAddress(
                 calendarUserAddress
             )
             if principal is None:
@@ -1670,7 +1683,7 @@
             proxyprivs = list(userprivs)
             proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))
 
-            principal = self.principalForUID(invite.shareeUID)
+            principal = yield self.principalForUID(invite.shareeUID)
             aces += (
                 # Inheritable specific access for the resource's associated principal.
                 davxml.ACE(
@@ -1763,11 +1776,12 @@
         return succeed(davPrivilegeSet)
 
 
+    @inlineCallbacks
     def defaultAccessControlList(self):
         """
         Only read privileges allowed for managed attachments.
         """
-        myPrincipal = self.parent.principalForRecord()
+        myPrincipal = yield self.parent.principalForRecord()
 
         read_privs = (
             davxml.Privilege(davxml.Read()),
@@ -1808,12 +1822,12 @@
                 ),
             )
 
-        return davxml.ACL(*aces)
+        returnValue(davxml.ACL(*aces))
 
 
     def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
         # Permissions here are fixed, and are not subject to inheritance rules, etc.
-        return succeed(self.defaultAccessControlList())
+        return self.defaultAccessControlList()
 
 
 
@@ -1927,7 +1941,7 @@
         cuas = (yield self._newStoreCalendarObject.component()).getAttendees()
         newACEs = []
         for calendarUserAddress in cuas:
-            principal = self.principalForCalendarUserAddress(
+            principal = yield self.principalForCalendarUserAddress(
                 calendarUserAddress
             )
             if principal is None:
@@ -1982,15 +1996,13 @@
         """
         if invite.mode in (_BIND_MODE_DIRECT,):
             ownerUID = invite.ownerUID
-            owner = self.principalForUID(ownerUID)
+            owner = yield self.principalForUID(ownerUID)
             shareeUID = invite.shareeUID
-            if owner.record.recordType == WikiDirectoryService.recordType_wikis:
+            if owner.record.recordType == WikiRecordType.macOSXServerWiki:
                 # Access level comes from what the wiki has granted to the
                 # sharee
-                sharee = self.principalForUID(shareeUID)
-                userID = sharee.record.guid
-                wikiID = owner.record.shortNames[0]
-                access = (yield getWikiAccess(userID, wikiID))
+                sharee = yield self.principalForUID(shareeUID)
+                access = (yield owner.record.accessForRecord(sharee.record))
                 if access == "read":
                     returnValue("read-only")
                 elif access in ("write", "admin"):
@@ -2026,7 +2038,7 @@
             if access in ("read-only", "read-write",):
                 userprivs.extend(privileges)
 
-            principal = self.principalForUID(invite.shareeUID)
+            principal = yield self.principalForUID(invite.shareeUID)
             aces += (
                 # Inheritable specific access for the resource's associated principal.
                 davxml.ACE(
@@ -2865,7 +2877,7 @@
                 principalURL = str(authz_principal)
                 if principalURL:
                     authz = (yield request.locateResource(principalURL))
-                    self._parentResource._newStoreObject._txn._authz_uid = authz.record.guid
+                    self._parentResource._newStoreObject._txn._authz_uid = authz.record.uid
 
             try:
                 response = (yield self.storeComponent(component, smart_merge=schedule_tag_match))
@@ -3586,7 +3598,7 @@
                 principalURL = str(authz_principal)
                 if principalURL:
                     authz = (yield request.locateResource(principalURL))
-                    self._parentResource._newStoreObject._txn._authz_uid = authz.record.guid
+                    self._parentResource._newStoreObject._txn._authz_uid = authz.record.uid
 
             try:
                 response = (yield self.storeComponent(component))
@@ -3695,7 +3707,7 @@
         else:
             userprivs.append(davxml.Privilege(davxml.WriteProperties()))
 
-        sharee = self.principalForUID(self._newStoreObject.viewerHome().uid())
+        sharee = yield self.principalForUID(self._newStoreObject.viewerHome().uid())
         aces = (
             # Inheritable specific access for the resource's associated principal.
             davxml.ACE(
@@ -3897,7 +3909,7 @@
         assert ignored is None, "This is a notification object, not a notification"
         jsondata = (yield self._newStoreObject.notificationData())
         if jsondata["notification-type"] == "invite-notification":
-            ownerPrincipal = self.principalForUID(jsondata["owner"])
+            ownerPrincipal = yield self.principalForUID(jsondata["owner"])
             ownerCN = ownerPrincipal.displayName()
             ownerHomeURL = ownerPrincipal.calendarHomeURLs()[0] if jsondata["shared-type"] == "calendar" else ownerPrincipal.addressBookHomeURLs()[0]
 
@@ -3907,7 +3919,7 @@
             else:
                 owner = "urn:uuid:" + ownerPrincipal.principalUID()
 
-            shareePrincipal = self.principalForUID(jsondata["sharee"])
+            shareePrincipal = yield self.principalForUID(jsondata["sharee"])
 
             if "supported-components" in jsondata:
                 comps = jsondata["supported-components"]
@@ -3942,10 +3954,10 @@
                 ),
             )
         elif jsondata["notification-type"] == "invite-reply":
-            ownerPrincipal = self.principalForUID(jsondata["owner"])
+            ownerPrincipal = yield self.principalForUID(jsondata["owner"])
             ownerHomeURL = ownerPrincipal.calendarHomeURLs()[0] if jsondata["shared-type"] == "calendar" else ownerPrincipal.addressBookHomeURLs()[0]
 
-            shareePrincipal = self.principalForUID(jsondata["sharee"])
+            shareePrincipal = yield self.principalForUID(jsondata["sharee"])
 
             # FIXME:  use urn:uuid always?
             if jsondata["shared-type"] == "calendar":
@@ -3959,7 +3971,7 @@
                 cua = "urn:uuid:" + shareePrincipal.principalUID()
 
             commonName = shareePrincipal.displayName()
-            record = shareePrincipal.record
+            # record = shareePrincipal.record
 
             typeAttr = {"shared-type": jsondata["shared-type"]}
             xmldata = customxml.Notification(
@@ -3973,9 +3985,9 @@
                     customxml.InReplyTo.fromString(jsondata["in-reply-to"]),
                     customxml.InviteSummary.fromString(jsondata["summary"]) if jsondata["summary"] else None,
                     customxml.CommonName.fromString(commonName) if commonName else None,
-                    customxml.FirstNameProperty(record.firstName) if record.firstName else None,
-                    customxml.LastNameProperty(record.lastName) if record.lastName else None,
-                    #**typeAttr
+                    # customxml.FirstNameProperty(record.firstName) if record.firstName else None,
+                    # customxml.LastNameProperty(record.lastName) if record.lastName else None,
+                    **typeAttr
                 ),
             )
         else:

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookmultiget.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookmultiget.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookmultiget.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -31,7 +31,10 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from txdav.xml import element as davxml
+from twext.who.idirectory import RecordType
 
+
+
 class AddressBookMultiget (StoreTestCase):
     """
     addressbook-multiget REPORT
@@ -39,6 +42,13 @@
     data_dir = os.path.join(os.path.dirname(__file__), "data")
     vcards_dir = os.path.join(data_dir, "vCards")
 
+
+    @inlineCallbacks
+    def setUp(self):
+        yield StoreTestCase.setUp(self)
+        self.authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+
+
     def test_multiget_some_vcards(self):
         """
         All vcards.
@@ -207,7 +217,7 @@
 </D:set>
 </D:mkcol>
 """
-            response = yield self.send(SimpleStoreRequest(self, "MKCOL", addressbook_uri, content=mkcol, authid="wsanchez"))
+            response = yield self.send(SimpleStoreRequest(self, "MKCOL", addressbook_uri, content=mkcol, authRecord=self.authRecord))
 
             response = IResponse(response)
 
@@ -221,7 +231,7 @@
                         "PUT",
                         joinURL(addressbook_uri, filename + ".vcf"),
                         headers=Headers({"content-type": MimeType.fromString("text/vcard")}),
-                        authid="wsanchez"
+                        authRecord=self.authRecord
                     )
                     request.stream = MemoryStream(icaldata)
                     yield self.send(request)
@@ -235,12 +245,12 @@
                         "PUT",
                         joinURL(addressbook_uri, child.basename()),
                         headers=Headers({"content-type": MimeType.fromString("text/vcard")}),
-                        authid="wsanchez"
+                        authRecord=self.authRecord
                     )
                     request.stream = MemoryStream(child.getContent())
                     yield self.send(request)
 
-        request = SimpleStoreRequest(self, "REPORT", addressbook_uri, authid="wsanchez")
+        request = SimpleStoreRequest(self, "REPORT", addressbook_uri, authRecord=self.authRecord)
         request.stream = MemoryStream(query.toxml())
         response = yield self.send(request)
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookquery.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookquery.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_addressbookquery.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -27,7 +27,10 @@
 from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.filepath import FilePath
+from twext.who.idirectory import RecordType
 
+
+
 class AddressBookQuery(StoreTestCase):
     """
     addressbook-query REPORT
@@ -67,6 +70,7 @@
 
         oldValue = config.MaxQueryWithDataResults
         config.MaxQueryWithDataResults = 1
+
         def _restoreValueOK(f):
             config.MaxQueryWithDataResults = oldValue
             return None
@@ -89,6 +93,7 @@
 
         oldValue = config.MaxQueryWithDataResults
         config.MaxQueryWithDataResults = 1
+
         def _restoreValueOK(f):
             config.MaxQueryWithDataResults = oldValue
             return None
@@ -191,15 +196,16 @@
         if response.code != responsecode.CREATED:
             self.fail("MKCOL failed: %s" % (response.code,))
         '''
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         # Add vCards to addressbook
         for child in FilePath(self.vcards_dir).children():
             if os.path.splitext(child.basename())[1] != ".vcf":
                 continue
-            request = SimpleStoreRequest(self, "PUT", joinURL(addressbook_uri, child.basename()), authid="wsanchez")
+            request = SimpleStoreRequest(self, "PUT", joinURL(addressbook_uri, child.basename()), authRecord=authRecord)
             request.stream = MemoryStream(child.getContent())
             yield self.send(request)
 
-        request = SimpleStoreRequest(self, "REPORT", addressbook_uri, authid="wsanchez")
+        request = SimpleStoreRequest(self, "REPORT", addressbook_uri, authRecord=authRecord)
         request.stream = MemoryStream(query.toxml())
         response = yield self.send(request)
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_cache.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_cache.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_cache.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -63,6 +63,9 @@
 
 class StubDirectory(object):
 
+    def oldNameToRecordType(self, oldName):
+        return oldName
+
     def recordWithShortName(self, recordType, recordName):
         return StubDirectoryRecord(recordName)
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_calendarquery.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_calendarquery.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -34,8 +34,8 @@
 from pycalendar.datetime import DateTime
 from twistedcaldav.ical import Component
 from txdav.caldav.icalendarstore import ComponentUpdateState
-from twistedcaldav.directory.directory import DirectoryService
 from txdav.caldav.datastore.query.filter import TimeRange
+from twext.who.idirectory import RecordType
 
 
 @inlineCallbacks
@@ -79,7 +79,7 @@
         """
         Put the contents of the Holidays directory into the store.
         """
-        record = self.directory.recordWithShortName(DirectoryService.recordType_users, "wsanchez")
+        record = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         yield self.transactionUnderTest().calendarHomeWithUID(record.uid, create=True)
         calendar = yield self.calendarUnderTest(name="calendar", home=record.uid)
         for f in os.listdir(self.holidays_dir):
@@ -248,6 +248,7 @@
         """
 
         self.patch(config, "MaxQueryWithDataResults", 1)
+
         def _restoreValueOK(f):
             self.fail("REPORT must fail with 403")
 
@@ -268,6 +269,7 @@
         """
 
         self.patch(config, "MaxQueryWithDataResults", 1)
+
         def _restoreValueError(f):
             self.fail("REPORT must not fail with 403")
 
@@ -343,7 +345,8 @@
     @inlineCallbacks
     def calendar_query(self, query, got_xml):
 
-        request = SimpleStoreRequest(self, "REPORT", "/calendars/users/wsanchez/calendar/", authid="wsanchez")
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+        request = SimpleStoreRequest(self, "REPORT", "/calendars/users/wsanchez/calendar/", authRecord=authRecord)
         request.stream = MemoryStream(query.toxml())
         response = yield self.send(request)
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_collectioncontents.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_collectioncontents.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -14,22 +14,22 @@
 # limitations under the License.
 ##
 
-from twisted.internet.defer import inlineCallbacks
 from twext.python.filepath import CachingFilePath as FilePath
-from txweb2 import responsecode
-from txweb2.iweb import IResponse
-from txweb2.stream import MemoryStream, FileStream
-from txweb2.http_headers import MimeType
-
+from twext.who.idirectory import RecordType
+from twisted.internet.defer import inlineCallbacks
 from twistedcaldav.ical import Component
 from twistedcaldav.memcachelock import MemcacheLock
 from twistedcaldav.memcacher import Memcacher
-
-
 from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
-from txweb2.dav.util import joinURL
 from txdav.caldav.datastore.sql import CalendarObject
+from txweb2 import responsecode
+from txweb2.dav.util import joinURL
+from txweb2.http_headers import MimeType
+from txweb2.iweb import IResponse
+from txweb2.stream import MemoryStream, FileStream
 
+
+
 class CollectionContents(StoreTestCase):
     """
     PUT request
@@ -52,7 +52,7 @@
         def _fakeDoImplicitScheduling(self, component, inserting, internal_state):
             return False, None, False, None
 
-        self.patch(CalendarObject , "doImplicitScheduling",
+        self.patch(CalendarObject, "doImplicitScheduling",
                    _fakeDoImplicitScheduling)
 
         # Tests in this suite assume that the root resource is a calendar home.
@@ -61,31 +61,27 @@
         return super(CollectionContents, self).setUp()
 
 
+    @inlineCallbacks
     def test_collection_in_calendar(self):
         """
         Make (regular) collection in calendar
         """
         calendar_uri = "/calendars/users/wsanchez/collection_in_calendar/"
 
-        def mkcalendar_cb(response):
-            response = IResponse(response)
-
-            if response.code != responsecode.CREATED:
-                self.fail("MKCALENDAR failed: %s" % (response.code,))
-
-            def mkcol_cb(response):
-                response = IResponse(response)
-
-                if response.code != responsecode.FORBIDDEN:
-                    self.fail("Incorrect response to nested MKCOL: %s" % (response.code,))
-
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authRecord=authRecord)
+        response = yield self.send(request)
+        response = IResponse(response)
+        if response.code != responsecode.CREATED:
+            self.fail("MKCALENDAR failed: %s" % (response.code,))
             nested_uri = joinURL(calendar_uri, "nested")
 
-            request = SimpleStoreRequest(self, "MKCOL", nested_uri, authid="wsanchez")
-            return self.send(request, mkcol_cb)
+            request = SimpleStoreRequest(self, "MKCOL", nested_uri, authRecord=authRecord)
+            response = yield self.send(request)
+            response = IResponse(response)
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authid="wsanchez")
-        return self.send(request, mkcalendar_cb)
+            if response.code != responsecode.FORBIDDEN:
+                self.fail("Incorrect response to nested MKCOL: %s" % (response.code,))
 
 
     def test_bogus_file(self):
@@ -163,6 +159,7 @@
         )
 
 
+    @inlineCallbacks
     def _test_file_in_calendar(self, what, *work):
         """
         Creates a calendar collection, then PUTs a resource into that collection
@@ -171,68 +168,58 @@
         """
         calendar_uri = "/calendars/users/wsanchez/testing_calendar/"
 
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authRecord=authRecord)
+        response = yield self.send(request)
+        response = IResponse(response)
+        if response.code != responsecode.CREATED:
+            self.fail("MKCALENDAR failed: %s" % (response.code,))
 
-        @inlineCallbacks
-        def mkcalendar_cb(response):
+        c = 0
+        for stream, response_code in work:
+            dst_uri = joinURL(calendar_uri, "dst%d.ics" % (c,))
+            request = SimpleStoreRequest(self, "PUT", dst_uri, authRecord=authRecord)
+            request.headers.setHeader("if-none-match", "*")
+            request.headers.setHeader("content-type", MimeType("text", "calendar"))
+            request.stream = stream
+            response = yield self.send(request)
             response = IResponse(response)
 
-            if response.code != responsecode.CREATED:
-                self.fail("MKCALENDAR failed: %s" % (response.code,))
+            if response.code != response_code:
+                self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
 
-            c = 0
+            c += 1
 
-            for stream, response_code in work:
 
-                dst_uri = joinURL(calendar_uri, "dst%d.ics" % (c,))
-                request = SimpleStoreRequest(self, "PUT", dst_uri, authid="wsanchez")
-                request.headers.setHeader("if-none-match", "*")
-                request.headers.setHeader("content-type", MimeType("text", "calendar"))
-                request.stream = stream
-                response = yield self.send(request)
-                response = IResponse(response)
 
-                if response.code != response_code:
-                    self.fail("Incorrect response to %s: %s (!= %s)" % (what, response.code, response_code))
-
-                c += 1
-
-        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authid="wsanchez")
-        return self.send(request, mkcalendar_cb)
-
-
+    @inlineCallbacks
     def test_fail_dot_file_put_in_calendar(self):
         """
         Make (regular) collection in calendar
         """
         calendar_uri = "/calendars/users/wsanchez/dot_file_in_calendar/"
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authRecord=authRecord)
+        response = yield self.send(request)
+        response = IResponse(response)
+        if response.code != responsecode.CREATED:
+            self.fail("MKCALENDAR failed: %s" % (response.code,))
 
-        def mkcalendar_cb(response):
-            response = IResponse(response)
+        stream = self.dataPath.child(
+            "Holidays").child(
+            "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"
+        ).open()
+        try:
+            calendar = str(Component.fromStream(stream))
+        finally:
+            stream.close()
 
-            if response.code != responsecode.CREATED:
-                self.fail("MKCALENDAR failed: %s" % (response.code,))
+        event_uri = "/".join([calendar_uri, ".event.ics"])
 
-            def put_cb(response):
-                response = IResponse(response)
-
-                if response.code != responsecode.FORBIDDEN:
-                    self.fail("Incorrect response to dot file PUT: %s" % (response.code,))
-
-            stream = self.dataPath.child(
-                "Holidays").child(
-                "C318AA54-1ED0-11D9-A5E0-000A958A3252.ics"
-            ).open()
-            try:
-                calendar = str(Component.fromStream(stream))
-            finally:
-                stream.close()
-
-            event_uri = "/".join([calendar_uri, ".event.ics"])
-
-            request = SimpleStoreRequest(self, "PUT", event_uri, authid="wsanchez")
-            request.headers.setHeader("content-type", MimeType("text", "calendar"))
-            request.stream = MemoryStream(calendar)
-            return self.send(request, put_cb)
-
-        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authid="wsanchez")
-        return self.send(request, mkcalendar_cb)
+        request = SimpleStoreRequest(self, "PUT", event_uri, authRecord=authRecord)
+        request.headers.setHeader("content-type", MimeType("text", "calendar"))
+        request.stream = MemoryStream(calendar)
+        response = yield self.send(request)
+        response = IResponse(response)
+        if response.code != responsecode.FORBIDDEN:
+            self.fail("Incorrect response to dot file PUT: %s" % (response.code,))

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_config.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_config.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_config.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -92,9 +92,10 @@
 
     def testDefaults(self):
         for key, value in DEFAULT_CONFIG.iteritems():
-            if key in ("ServerHostName", "Notifications", "MultiProcess",
-                "Postgres"):
-                # Value is calculated and may vary
+            if key in (
+                "ServerHostName", "Notifications", "MultiProcess",
+                "Postgres", "DirectoryRealmName"
+            ):  # Value is calculated and may vary
                 continue
             for item in RELATIVE_PATHS:
                 item = item[1]

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_icalendar.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_icalendar.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,6 +19,7 @@
 import itertools
 
 from twisted.trial.unittest import SkipTest
+from twisted.internet.defer import inlineCallbacks, succeed
 
 from twistedcaldav.ical import Component, Property, InvalidICalendarDataError, \
     normalizeCUAddress, normalize_iCalStr
@@ -32,6 +33,8 @@
 from twistedcaldav.dateops import normalizeForExpand
 from pycalendar.value import Value
 
+
+
 class iCalendar (twistedcaldav.test.util.TestCase):
     """
     iCalendar support tests
@@ -7497,6 +7500,7 @@
             self.assertEquals(expected, ical.hasInstancesAfter(cutoff))
 
 
+    @inlineCallbacks
     def test_normalizeCalendarUserAddressesFromUUID(self):
         """
         Ensure mailto is preferred, followed by path form, then http form.
@@ -7520,25 +7524,27 @@
 
 
         def lookupFunction(cuaddr, ignored1, ignored2):
-            return {
-                "urn:uuid:foo" : (
-                    "Foo",
-                    "foo",
-                    ("urn:uuid:foo", "http://example.com/foo", "/foo")
-                ),
-                "urn:uuid:bar" : (
-                    "Bar",
-                    "bar",
-                    ("urn:uuid:bar", "mailto:bar at example.com", "http://example.com/bar", "/bar")
-                ),
-                "urn:uuid:baz" : (
-                    "Baz",
-                    "baz",
-                    ("urn:uuid:baz", "http://example.com/baz")
-                ),
-            }[cuaddr]
+            return succeed(
+                {
+                    "urn:uuid:foo" : (
+                        "Foo",
+                        "foo",
+                        ("urn:uuid:foo", "http://example.com/foo", "/foo")
+                    ),
+                    "urn:uuid:bar" : (
+                        "Bar",
+                        "bar",
+                        ("urn:uuid:bar", "mailto:bar at example.com", "http://example.com/bar", "/bar")
+                    ),
+                    "urn:uuid:baz" : (
+                        "Baz",
+                        "baz",
+                        ("urn:uuid:baz", "http://example.com/baz")
+                    ),
+                }[cuaddr]
+            )
 
-        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=False)
+        yield component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=False)
 
         self.assertEquals("mailto:bar at example.com",
             component.getAttendeeProperty(("mailto:bar at example.com",)).value())
@@ -7548,6 +7554,7 @@
             component.getAttendeeProperty(("http://example.com/baz",)).value())
 
 
+    @inlineCallbacks
     def test_normalizeCalendarUserAddressesAndLocationChange(self):
         """
         Ensure http(s) and /path CUA values are tucked away into the property
@@ -7573,25 +7580,27 @@
 
 
         def lookupFunction(cuaddr, ignored1, ignored2):
-            return {
-                "/principals/users/foo" : (
-                    "Foo",
-                    "foo",
-                    ("urn:uuid:foo",)
-                ),
-                "http://example.com/principals/users/bar" : (
-                    "Bar",
-                    "bar",
-                    ("urn:uuid:bar",)
-                ),
-                "http://example.com/principals/locations/buzz" : (
-                    "{Restricted} Buzz",
-                    "buzz",
-                    ("urn:uuid:buzz",)
-                ),
-            }[cuaddr]
+            return succeed(
+                {
+                    "/principals/users/foo" : (
+                        "Foo",
+                        "foo",
+                        ("urn:uuid:foo",)
+                    ),
+                    "http://example.com/principals/users/bar" : (
+                        "Bar",
+                        "bar",
+                        ("urn:uuid:bar",)
+                    ),
+                    "http://example.com/principals/locations/buzz" : (
+                        "{Restricted} Buzz",
+                        "buzz",
+                        ("urn:uuid:buzz",)
+                    ),
+                }[cuaddr]
+            )
 
-        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+        yield component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
 
         # Location value changed
         prop = component.mainComponent().getProperty("LOCATION")
@@ -7601,6 +7610,7 @@
         self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
 
 
+    @inlineCallbacks
     def test_normalizeCalendarUserAddressesAndLocationNoChange(self):
         """
         Ensure http(s) and /path CUA values are tucked away into the property
@@ -7626,25 +7636,27 @@
 
 
         def lookupFunction(cuaddr, ignored1, ignored2):
-            return {
-                "/principals/users/foo" : (
-                    "Foo",
-                    "foo",
-                    ("urn:uuid:foo",)
-                ),
-                "http://example.com/principals/users/bar" : (
-                    "Bar",
-                    "bar",
-                    ("urn:uuid:bar",)
-                ),
-                "http://example.com/principals/locations/buzz" : (
-                    "{Restricted} Buzz",
-                    "buzz",
-                    ("urn:uuid:buzz",)
-                ),
-            }[cuaddr]
+            return succeed(
+                {
+                    "/principals/users/foo" : (
+                        "Foo",
+                        "foo",
+                        ("urn:uuid:foo",)
+                    ),
+                    "http://example.com/principals/users/bar" : (
+                        "Bar",
+                        "bar",
+                        ("urn:uuid:bar",)
+                    ),
+                    "http://example.com/principals/locations/buzz" : (
+                        "{Restricted} Buzz",
+                        "buzz",
+                        ("urn:uuid:buzz",)
+                    ),
+                }[cuaddr]
+            )
 
-        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+        yield component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
 
         # Location value changed
         prop = component.mainComponent().getProperty("LOCATION")
@@ -7654,6 +7666,7 @@
         self.assertEquals(prop.parameterValue("CN"), "{Restricted} Buzz")
 
 
+    @inlineCallbacks
     def test_normalizeCalendarUserAddressesAndLocationNoChangeOtherCUType(self):
         """
         Ensure http(s) and /path CUA values are tucked away into the property
@@ -7679,25 +7692,27 @@
 
 
         def lookupFunction(cuaddr, ignored1, ignored2):
-            return {
-                "/principals/users/foo" : (
-                    "Foo",
-                    "foo",
-                    ("urn:uuid:foo",)
-                ),
-                "http://example.com/principals/users/bar" : (
-                    "Bar",
-                    "bar",
-                    ("urn:uuid:bar",)
-                ),
-                "http://example.com/principals/locations/buzz" : (
-                    "{Restricted} Buzz",
-                    "buzz",
-                    ("urn:uuid:buzz",)
-                ),
-            }[cuaddr]
+            return succeed(
+                {
+                    "/principals/users/foo" : (
+                        "Foo",
+                        "foo",
+                        ("urn:uuid:foo",)
+                    ),
+                    "http://example.com/principals/users/bar" : (
+                        "Bar",
+                        "bar",
+                        ("urn:uuid:bar",)
+                    ),
+                    "http://example.com/principals/locations/buzz" : (
+                        "{Restricted} Buzz",
+                        "buzz",
+                        ("urn:uuid:buzz",)
+                    ),
+                }[cuaddr]
+            )
 
-        component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
+        yield component.normalizeCalendarUserAddresses(lookupFunction, None, toUUID=True)
 
         # Location value changed
         prop = component.mainComponent().getProperty("LOCATION")
@@ -8404,6 +8419,7 @@
             self.assertEqual(changed, result_changed)
 
 
+    @inlineCallbacks
     def test_normalizeCUAddressFromUUID(self):
         """
         Ensure mailto is preferred, followed by path form, then http form.
@@ -8418,34 +8434,37 @@
         )
 
         def lookupFunction(cuaddr, ignored1, ignored2):
-            return {
-                "urn:uuid:foo" : (
-                    "Foo",
-                    "foo",
-                    ("urn:uuid:foo", "http://example.com/foo", "/foo")
-                ),
-                "urn:uuid:bar" : (
-                    "Bar",
-                    "bar",
-                    ("urn:uuid:bar", "mailto:bar at example.com", "http://example.com/bar", "/bar")
-                ),
-                "urn:uuid:baz" : (
-                    "Baz",
-                    "baz",
-                    ("urn:uuid:baz", "http://example.com/baz")
-                ),
-                "urn:uuid:buz" : (
-                    "Buz",
-                    "buz",
-                    ("urn:uuid:buz",)
-                ),
-            }[cuaddr]
+            return succeed(
+                {
+                    "urn:uuid:foo" : (
+                        "Foo",
+                        "foo",
+                        ("urn:uuid:foo", "http://example.com/foo", "/foo")
+                    ),
+                    "urn:uuid:bar" : (
+                        "Bar",
+                        "bar",
+                        ("urn:uuid:bar", "mailto:bar at example.com", "http://example.com/bar", "/bar")
+                    ),
+                    "urn:uuid:baz" : (
+                        "Baz",
+                        "baz",
+                        ("urn:uuid:baz", "http://example.com/baz")
+                    ),
+                    "urn:uuid:buz" : (
+                        "Buz",
+                        "buz",
+                        ("urn:uuid:buz",)
+                    ),
+                }[cuaddr]
+            )
 
         for cuaddr, result in data:
-            new_cuaddr = normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=False)
+            new_cuaddr = yield normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=False)
             self.assertEquals(new_cuaddr, result)
 
 
+    @inlineCallbacks
     def test_normalizeCUAddressToUUID(self):
         """
         Ensure http(s) and /path CUA values are tucked away into the property
@@ -8459,21 +8478,23 @@
 
 
         def lookupFunction(cuaddr, ignored1, ignored2):
-            return {
-                "/principals/users/foo" : (
-                    "Foo",
-                    "foo",
-                    ("urn:uuid:foo",)
-                ),
-                "http://example.com/principals/users/buz" : (
-                    "Buz",
-                    "buz",
-                    ("urn:uuid:buz",)
-                ),
-            }[cuaddr]
+            return succeed(
+                {
+                    "/principals/users/foo" : (
+                        "Foo",
+                        "foo",
+                        ("urn:uuid:foo",)
+                    ),
+                    "http://example.com/principals/users/buz" : (
+                        "Buz",
+                        "buz",
+                        ("urn:uuid:buz",)
+                    ),
+                }[cuaddr]
+            )
 
         for cuaddr, result in data:
-            new_cuaddr = normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=True)
+            new_cuaddr = yield normalizeCUAddress(cuaddr, lookupFunction, None, toUUID=True)
             self.assertEquals(new_cuaddr, result)
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_mkcalendar.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_mkcalendar.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_mkcalendar.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -27,6 +27,10 @@
 from twistedcaldav import caldavxml
 from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
 
+from twext.who.idirectory import RecordType
+
+
+
 class MKCALENDAR (StoreTestCase):
     """
     MKCALENDAR request
@@ -35,6 +39,12 @@
     # Try nesting calendars (should fail)
     # HEAD request on calendar: resourcetype = (collection, calendar)
 
+    @inlineCallbacks
+    def setUp(self):
+        yield StoreTestCase.setUp(self)
+        self.authRecord = yield self.directory.recordWithShortName(RecordType.user, u"user01")
+
+
     def test_make_calendar(self):
         """
         Make calendar
@@ -45,7 +55,7 @@
         if os.path.exists(path):
             rmdir(path)
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", uri, authid="user01")
+        request = SimpleStoreRequest(self, "MKCALENDAR", uri, authRecord=self.authRecord)
 
         @inlineCallbacks
         def do_test(response):
@@ -146,7 +156,7 @@
             )
         )
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", uri, authid="user01")
+        request = SimpleStoreRequest(self, "MKCALENDAR", uri, authRecord=self.authRecord)
         request.stream = MemoryStream(mk.toxml())
         return self.send(request, do_test)
 
@@ -165,7 +175,7 @@
 
             # FIXME: Check for DAV:resource-must-be-null element
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", uri, authid="user01")
+        request = SimpleStoreRequest(self, "MKCALENDAR", uri, authRecord=self.authRecord)
         return self.send(request, do_test)
 
 
@@ -190,8 +200,8 @@
 
             nested_uri = os.path.join(first_uri, "nested")
 
-            request = SimpleStoreRequest(self, "MKCALENDAR", nested_uri, authid="user01")
+            request = SimpleStoreRequest(self, "MKCALENDAR", nested_uri, authRecord=self.authRecord)
             yield self.send(request, do_test)
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", first_uri, authid="user01")
+        request = SimpleStoreRequest(self, "MKCALENDAR", first_uri, authRecord=self.authRecord)
         return self.send(request, next)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_multiget.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_multiget.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -14,6 +14,7 @@
 ##
 
 from twext.python.filepath import CachingFilePath as FilePath
+from twext.who.idirectory import RecordType
 from txweb2 import responsecode
 from txweb2.dav.util import davXMLFromStream, joinURL
 from txweb2.http_headers import Headers, MimeType
@@ -38,6 +39,12 @@
     data_dir = os.path.join(os.path.dirname(__file__), "data")
     holidays_dir = os.path.join(data_dir, "Holidays")
 
+    @inlineCallbacks
+    def setUp(self):
+        yield StoreTestCase.setUp(self)
+        self.authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+
+
     def test_multiget_some_events(self):
         """
         All events.
@@ -262,7 +269,7 @@
     def calendar_query(self, calendar_uri, query, got_xml, data, no_init):
 
         if not no_init:
-            response = yield self.send(SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authid="wsanchez"))
+            response = yield self.send(SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authRecord=self.authRecord))
             response = IResponse(response)
             if response.code != responsecode.CREATED:
                 self.fail("MKCALENDAR failed: %s" % (response.code,))
@@ -274,7 +281,7 @@
                         "PUT",
                         joinURL(calendar_uri, filename + ".ics"),
                         headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
-                        authid="wsanchez"
+                        authRecord=self.authRecord
                     )
                     request.stream = MemoryStream(icaldata)
                     yield self.send(request)
@@ -288,12 +295,12 @@
                         "PUT",
                         joinURL(calendar_uri, child.basename()),
                         headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
-                        authid="wsanchez"
+                        authRecord=self.authRecord
                     )
                     request.stream = MemoryStream(child.getContent())
                     yield self.send(request)
 
-        request = SimpleStoreRequest(self, "REPORT", calendar_uri, authid="wsanchez")
+        request = SimpleStoreRequest(self, "REPORT", calendar_uri, authRecord=self.authRecord)
         request.stream = MemoryStream(query.toxml())
         response = yield self.send(request)
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_props.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_props.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_props.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,21 +19,35 @@
 from txweb2.iweb import IResponse
 from txweb2.stream import MemoryStream
 
+from twisted.internet.defer import inlineCallbacks
+
 from twistedcaldav import caldavxml
 from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
 
 from txdav.xml import element as davxml
 
+from twext.who.idirectory import RecordType
+
+
+
 class Properties(StoreTestCase):
     """
     CalDAV properties
     """
+
+    @inlineCallbacks
+    def setUp(self):
+        yield StoreTestCase.setUp(self)
+        self.authRecord = yield self.directory.recordWithShortName(RecordType.user, u"user01")
+
+
     def test_live_props(self):
         """
         Live CalDAV properties
         """
         calendar_uri = "/calendars/users/user01/test/"
 
+
         def mkcalendar_cb(response):
             response = IResponse(response)
 
@@ -123,24 +137,24 @@
                 return davXMLFromStream(response.stream).addCallback(got_xml)
 
             query = davxml.PropertyFind(
-                        davxml.PropertyContainer(
-                            caldavxml.SupportedCalendarData(),
-                            caldavxml.SupportedCalendarComponentSet(),
-                            davxml.SupportedReportSet(),
-                        ),
-                    )
+                davxml.PropertyContainer(
+                    caldavxml.SupportedCalendarData(),
+                    caldavxml.SupportedCalendarComponentSet(),
+                    davxml.SupportedReportSet(),
+                ),
+            )
 
             request = SimpleStoreRequest(
                 self,
                 "PROPFIND",
                 calendar_uri,
                 headers=http_headers.Headers({"Depth": "0"}),
-                authid="user01",
+                authRecord=self.authRecord,
             )
             request.stream = MemoryStream(query.toxml())
             return self.send(request, propfind_cb)
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authid="user01")
+        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authRecord=self.authRecord)
         return self.send(request, mkcalendar_cb)
 
 
@@ -207,10 +221,10 @@
                 "PROPFIND",
                 calendar_uri,
                 headers=http_headers.Headers({"Depth": "0"}),
-                authid="user01",
+                authRecord=self.authRecord,
             )
             request.stream = MemoryStream(query.toxml())
             return self.send(request, propfind_cb)
 
-        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authid="user01")
+        request = SimpleStoreRequest(self, "MKCALENDAR", calendar_uri, authRecord=self.authRecord)
         return self.send(request, mkcalendar_cb)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -14,22 +14,25 @@
 # limitations under the License.
 ##
 
+from twext.who.idirectory import RecordType
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.notifications import NotificationCollectionResource
+from twistedcaldav.resource import (
+    CalDAVResource, CommonHomeResource,
+    CalendarHomeResource, AddressBookHomeResource
+)
+from twistedcaldav.test.util import (
+    InMemoryPropertyStore, StoreTestCase, SimpleStoreRequest
+)
+from twistedcaldav.test.util import TestCase
 from txdav.xml.element import HRef, Principal, Unauthenticated
 from txweb2.http import HTTPError
 from txweb2.test.test_server import SimpleRequest
 
-from twisted.internet.defer import inlineCallbacks
 
-from twistedcaldav import carddavxml
-from twistedcaldav.config import config
-from twistedcaldav.resource import CalDAVResource, CommonHomeResource, \
- CalendarHomeResource, AddressBookHomeResource
-from twistedcaldav.test.util import InMemoryPropertyStore, StoreTestCase, \
-    SimpleStoreRequest
-from twistedcaldav.test.util import TestCase
-from twistedcaldav.notifications import NotificationCollectionResource
 
-
 class StubProperty(object):
     def qname(self):
         return "StubQnamespace", "StubQname"
@@ -185,13 +188,20 @@
 
 class DefaultAddressBook (StoreTestCase):
 
+
     @inlineCallbacks
+    def setUp(self):
+        yield StoreTestCase.setUp(self)
+        self.authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
+
+
+    @inlineCallbacks
     def test_pick_default_addressbook(self):
         """
         Get adbk
         """
 
-        request = SimpleStoreRequest(self, "GET", "/addressbooks/users/wsanchez/", authid="wsanchez")
+        request = SimpleStoreRequest(self, "GET", "/addressbooks/users/wsanchez/", authRecord=self.authRecord)
         home = yield request.locateResource("/addressbooks/users/wsanchez")
 
         # default property initially not present

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_sharing.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_sharing.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_sharing.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,30 +19,31 @@
 from txweb2.http_headers import MimeType
 from txweb2.iweb import IResponse
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twistedcaldav import customxml
-from twistedcaldav import sharing
 from twistedcaldav.config import config
 from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.sharing import WikiDirectoryService
 from twistedcaldav.test.test_cache import StubResponseCacheResource
 from twistedcaldav.test.util import norequest, StoreTestCase, SimpleStoreRequest
 
-from txdav.caldav.datastore.test.util import buildDirectory
 from txdav.common.datastore.sql_tables import _BIND_MODE_DIRECT
 from txdav.xml import element as davxml
 from txdav.xml.parser import WebDAVDocument
 
 from xml.etree.cElementTree import XML
+from txdav.who.wiki import (
+    DirectoryRecord as WikiDirectoryRecord,
+    DirectoryService as WikiDirectoryService,
+    RecordType as WikiRecordType,
+    WikiAccessLevel
+)
 
+sharedOwnerType = davxml.ResourceType.sharedownercalendar  # @UndefinedVariable
+regularCalendarType = davxml.ResourceType.calendar  # @UndefinedVariable
 
-sharedOwnerType = davxml.ResourceType.sharedownercalendar #@UndefinedVariable
-regularCalendarType = davxml.ResourceType.calendar #@UndefinedVariable
 
 
-
 def normalize(x):
     """
     Normalize some XML by parsing it, collapsing whitespace, and
@@ -63,8 +64,8 @@
         self.fullName = name
         self.guid = name
         self.calendarUserAddresses = set((cuaddr,))
-        if name.startswith("wiki-"):
-            recordType = WikiDirectoryService.recordType_wikis
+        if name.startswith(WikiDirectoryService.uidPrefix):
+            recordType = WikiRecordType.macOSXServerWiki
         else:
             recordType = None
         self.recordType = recordType
@@ -131,42 +132,45 @@
         super(SharingTests, self).configure()
         self.patch(config.Sharing, "Enabled", True)
         self.patch(config.Sharing.Calendars, "Enabled", True)
+        self.patch(config.Authentication.Wiki, "Enabled", True)
 
 
     @inlineCallbacks
     def setUp(self):
         yield super(SharingTests, self).setUp()
 
-        def patched(c):
-            """
-            The decorated method is patched on L{CalDAVResource} for the
-            duration of the test.
-            """
-            self.patch(CalDAVResource, c.__name__, c)
-            return c
+        # FIXME: not sure what these were for:
 
-        @patched
-        def principalForCalendarUserAddress(resourceSelf, cuaddr):
-            if "bogus" in cuaddr:
-                return None
-            else:
-                return FakePrincipal(cuaddr, self)
+        #     def patched(c):
+        #         """
+        #         The decorated method is patched on L{CalDAVResource} for the
+        #         duration of the test.
+        #         """
+        #         self.patch(CalDAVResource, c.__name__, c)
+        #         return c
 
-        @patched
-        def validUserIDForShare(resourceSelf, userid, request):
-            """
-            Temporary replacement for L{CalDAVResource.validUserIDForShare}
-            that marks any principal without 'bogus' in its name.
-            """
-            result = principalForCalendarUserAddress(resourceSelf, userid)
-            if result is None:
-                return result
-            return result.principalURL()
+        #     @patched
+        #     def principalForCalendarUserAddress(resourceSelf, cuaddr):
+        #         if "bogus" in cuaddr:
+        #             return None
+        #         else:
+        #             return FakePrincipal(cuaddr, self)
 
-        @patched
-        def principalForUID(resourceSelf, principalUID):
-            return FakePrincipal("urn:uuid:" + principalUID, self)
+        #     @patched
+        #     def validUserIDForShare(resourceSelf, userid, request):
+        #         """
+        #         Temporary replacement for L{CalDAVResource.validUserIDForShare}
+        #         that marks any principal without 'bogus' in its name.
+        #         """
+        #         result = principalForCalendarUserAddress(resourceSelf, userid)
+        #         if result is None:
+        #             return result
+        #         return result.principalURL()
 
+        #     @patched
+        #     def principalForUID(resourceSelf, principalUID):
+        #         return FakePrincipal("urn:uuid:" + principalUID, self)
+
         self.resource = yield self._getResource()
 
 
@@ -185,7 +189,8 @@
 
     @inlineCallbacks
     def _doPOST(self, body, resultcode=responsecode.OK):
-        request = SimpleStoreRequest(self, "POST", "/calendars/__uids__/user01/calendar/", content=body, authid="user01")
+        authRecord = yield self.directory.recordWithUID(u"user01")
+        request = SimpleStoreRequest(self, "POST", "/calendars/__uids__/user01/calendar/", content=body, authRecord=authRecord)
         request.headers.setHeader("content-type", MimeType("text", "xml"))
         response = yield self.send(request)
         response = IResponse(response)
@@ -210,7 +215,8 @@
 
     @inlineCallbacks
     def _doPOSTSharerAccept(self, body, resultcode=responsecode.OK):
-        request = SimpleStoreRequest(self, "POST", "/calendars/__uids__/user02/", content=body, authid="user02")
+        authRecord = yield self.directory.recordWithUID(u"user02")
+        request = SimpleStoreRequest(self, "POST", "/calendars/__uids__/user02/", content=body, authRecord=authRecord)
         request.headers.setHeader("content-type", MimeType("text", "xml"))
         response = yield self.send(request)
         response = IResponse(response)
@@ -319,7 +325,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             )
@@ -349,7 +355,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             )
@@ -398,7 +404,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadAccess()),
                 customxml.InviteStatusNoResponse(),
             )
@@ -473,21 +479,21 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user03"),
-                customxml.CommonName.fromString("USER03"),
+                customxml.CommonName.fromString("User 03"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user04"),
-                customxml.CommonName.fromString("USER04"),
+                customxml.CommonName.fromString("User 04"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
@@ -531,14 +537,14 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user04"),
-                customxml.CommonName.fromString("USER04"),
+                customxml.CommonName.fromString("User 04"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
@@ -582,14 +588,14 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user03"),
-                customxml.CommonName.fromString("USER03"),
+                customxml.CommonName.fromString("User 03"),
                 customxml.InviteAccess(customxml.ReadAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
@@ -699,7 +705,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             )
@@ -735,23 +741,23 @@
     @inlineCallbacks
     def wikiSetup(self):
         """
-        Create a wiki called C{wiki-testing}, and share it with the user whose
+        Create a wiki called C{[wiki]testing}, and share it with the user whose
         home is at /.  Return the name of the newly shared calendar in the
         sharee's home.
         """
 
-        self._sqlCalendarStore._directoryService = buildDirectory(homes=("wiki-testing",))
         wcreate = self._sqlCalendarStore.newTransaction("create wiki")
-        yield wcreate.calendarHomeWithUID("wiki-testing", create=True)
+        yield wcreate.calendarHomeWithUID(
+            u"{prefix}testing".format(prefix=WikiDirectoryService.uidPrefix),
+            create=True
+        )
         yield wcreate.commit()
 
-        newService = WikiDirectoryService()
-        newService.realmName = self.directory.realmName
-        self.directory.addService(newService)
-
         txn = self.transactionUnderTest()
-        sharee = yield self.homeUnderTest(name="user01")
-        sharer = yield txn.calendarHomeWithUID("wiki-testing")
+        sharee = yield self.homeUnderTest(name="user01", create=True)
+        sharer = yield txn.calendarHomeWithUID(
+            u"{prefix}testing".format(prefix=WikiDirectoryService.uidPrefix),
+        )
         cal = yield sharer.calendarWithName("calendar")
         sharedName = yield cal.shareWith(sharee, _BIND_MODE_DIRECT)
         returnValue(sharedName)
@@ -764,13 +770,14 @@
         to the sharee, so that delegates of the sharee get the same level of
         access.
         """
+        sharedName = yield self.wikiSetup()
+        access = WikiAccessLevel.read
 
-        access = "read"
-        def stubWikiAccessMethod(userID, wikiID):
-            return access
-        self.patch(sharing, "getWikiAccess", stubWikiAccessMethod)
+        def stubAccessForRecord(*args):
+            return succeed(access)
 
-        sharedName = yield self.wikiSetup()
+        self.patch(WikiDirectoryRecord, "accessForRecord", stubAccessForRecord)
+
         request = SimpleStoreRequest(self, "GET", "/calendars/__uids__/user01/")
         collection = yield request.locateResource("/calendars/__uids__/user01/" + sharedName)
 
@@ -779,7 +786,7 @@
         self.assertFalse("<write/>" in acl.toxml())
 
         # Simulate the wiki server granting Read-Write access
-        access = "write"
+        access = WikiAccessLevel.write
         acl = (yield collection.shareeAccessControlList(request))
         self.assertTrue("<write/>" in acl.toxml())
 
@@ -792,13 +799,17 @@
         un-share that collection.
         """
         sharedName = yield self.wikiSetup()
-        access = "write"
-        def stubWikiAccessMethod(userID, wikiID):
-            return access
-        self.patch(sharing, "getWikiAccess", stubWikiAccessMethod)
+        access = WikiAccessLevel.write
+
+        def stubAccessForRecord(*args):
+            return succeed(access)
+
+        self.patch(WikiDirectoryRecord, "accessForRecord", stubAccessForRecord)
+
         @inlineCallbacks
         def listChildrenViaPropfind():
-            request = SimpleStoreRequest(self, "PROPFIND", "/calendars/__uids__/user01/", authid="user01")
+            authRecord = yield self.directory.recordWithUID(u"user01")
+            request = SimpleStoreRequest(self, "PROPFIND", "/calendars/__uids__/user01/", authRecord=authRecord)
             request.headers.setHeader("depth", "1")
             response = yield self.send(request)
             response = IResponse(response)
@@ -810,9 +821,10 @@
             seq.remove(shortest)
             filtered = [elem[len(shortest):].rstrip("/") for elem in seq]
             returnValue(filtered)
+
         childNames = yield listChildrenViaPropfind()
         self.assertIn(sharedName, childNames)
-        access = "no-access"
+        access = WikiAccessLevel.none
         childNames = yield listChildrenViaPropfind()
         self.assertNotIn(sharedName, childNames)
 
@@ -837,7 +849,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
@@ -867,7 +879,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),
@@ -911,7 +923,7 @@
             customxml.InviteUser(
                 customxml.UID.fromString(""),
                 davxml.HRef.fromString("urn:uuid:user02"),
-                customxml.CommonName.fromString("USER02"),
+                customxml.CommonName.fromString("User 02"),
                 customxml.InviteAccess(customxml.ReadWriteAccess()),
                 customxml.InviteStatusNoResponse(),
             ),

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_upgrade.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_upgrade.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_upgrade.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -20,21 +20,22 @@
 import cPickle
 
 from twisted.python.reflect import namedClass
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, succeed
 
 from txdav.xml.parser import WebDAVDocument
 from txdav.caldav.datastore.index_file import db_basename
 
 from twistedcaldav.config import config
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
 from txdav.caldav.datastore.scheduling.imip.mailgateway import MailGatewayTokensDatabase
 from twistedcaldav.upgrade import (
     xattrname, upgradeData, updateFreeBusySet,
-    removeIllegalCharacters, normalizeCUAddrs
+    removeIllegalCharacters, normalizeCUAddrs,
+    loadDelegatesFromXML, migrateDelegatesToStore
 )
-from twistedcaldav.test.util import TestCase
-from calendarserver.tools.util import getDirectory
+from twistedcaldav.test.util import StoreTestCase
+from txdav.who.delegates import delegatesOf
+from twistedcaldav.directory.calendaruserproxy import ProxySqliteDB
 
 
 
@@ -51,33 +52,18 @@
 OLDPROXYFILE = ".db.calendaruserproxy"
 NEWPROXYFILE = "proxies.sqlite"
 
-class UpgradeTests(TestCase):
 
+class UpgradeTests(StoreTestCase):
 
-    def setUpXMLDirectory(self):
-        xmlFile = os.path.join(os.path.dirname(os.path.dirname(__file__)),
-            "directory", "test", "accounts.xml")
-        config.DirectoryService.params.xmlFile = xmlFile
 
-        xmlAugmentsFile = os.path.join(os.path.dirname(os.path.dirname(__file__)),
-            "directory", "test", "augments.xml")
-        config.AugmentService.type = "twistedcaldav.directory.augment.AugmentXMLDB"
-        config.AugmentService.params.xmlFiles = (xmlAugmentsFile,)
-
-        resourceFile = os.path.join(os.path.dirname(os.path.dirname(__file__)),
-            "directory", "test", "resources.xml")
-        config.ResourceService.params.xmlFile = resourceFile
-
-
     def doUpgrade(self, config):
         """
         Perform the actual upgrade.  (Hook for parallel tests.)
         """
-        return upgradeData(config)
+        return upgradeData(config, self.directory)
 
 
     def setUpInitialStates(self):
-        self.setUpXMLDirectory()
 
         self.setUpOldDocRoot()
         self.setUpOldDocRootWithoutDB()
@@ -202,7 +188,7 @@
         """
 
         self.setUpInitialStates()
-        directory = getDirectory()
+        directory = self.directory
 
         #
         # Verify these values require no updating:
@@ -210,18 +196,18 @@
 
         # Uncompressed XML
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/__uids__/BB05932F-DCE7-4195-9ED4-0896EAFF3B0B/calendar</href>\r\n</calendar-free-busy-set>\r\n"
-        self.assertEquals(updateFreeBusySet(value, directory), None)
+        self.assertEquals((yield updateFreeBusySet(value, directory)), None)
 
         # Zlib compressed XML
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/__uids__/BB05932F-DCE7-4195-9ED4-0896EAFF3B0B/calendar</href>\r\n</calendar-free-busy-set>\r\n"
         value = zlib.compress(value)
-        self.assertEquals(updateFreeBusySet(value, directory), None)
+        self.assertEquals((yield updateFreeBusySet(value, directory)), None)
 
         # Pickled XML
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/__uids__/BB05932F-DCE7-4195-9ED4-0896EAFF3B0B/calendar</href>\r\n</calendar-free-busy-set>\r\n"
         doc = WebDAVDocument.fromString(value)
         value = cPickle.dumps(doc.root_element)
-        self.assertEquals(updateFreeBusySet(value, directory), None)
+        self.assertEquals((yield updateFreeBusySet(value, directory)), None)
 
         #
         # Verify these values do require updating:
@@ -230,14 +216,14 @@
 
         # Uncompressed XML
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/users/wsanchez/calendar</href>\r\n</calendar-free-busy-set>\r\n"
-        newValue = updateFreeBusySet(value, directory)
+        newValue = yield updateFreeBusySet(value, directory)
         newValue = zlib.decompress(newValue)
         self.assertEquals(newValue, expected)
 
         # Zlib compressed XML
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/users/wsanchez/calendar</href>\r\n</calendar-free-busy-set>\r\n"
         value = zlib.compress(value)
-        newValue = updateFreeBusySet(value, directory)
+        newValue = yield updateFreeBusySet(value, directory)
         newValue = zlib.decompress(newValue)
         self.assertEquals(newValue, expected)
 
@@ -245,7 +231,7 @@
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/users/wsanchez/calendar</href>\r\n</calendar-free-busy-set>\r\n"
         doc = WebDAVDocument.fromString(value)
         value = cPickle.dumps(doc.root_element)
-        newValue = updateFreeBusySet(value, directory)
+        newValue = yield updateFreeBusySet(value, directory)
         newValue = zlib.decompress(newValue)
         self.assertEquals(newValue, expected)
 
@@ -254,7 +240,7 @@
         #
         expected = "<?xml version='1.0' encoding='UTF-8'?>\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'/>"
         value = "<?xml version='1.0' encoding='UTF-8'?>\r\n<calendar-free-busy-set xmlns='urn:ietf:params:xml:ns:caldav'>\r\n  <href xmlns='DAV:'>/calendars/users/nonexistent/calendar</href>\r\n</calendar-free-busy-set>\r\n"
-        newValue = updateFreeBusySet(value, directory)
+        newValue = yield updateFreeBusySet(value, directory)
         newValue = zlib.decompress(newValue)
         self.assertEquals(newValue, expected)
 
@@ -296,7 +282,6 @@
         The upgrade process should remove unused notification directories in
         users' calendar homes, as well as the XML files found therein.
         """
-        self.setUpXMLDirectory()
 
         before = {
             "calendars": {
@@ -306,7 +291,7 @@
                             db_basename : {
                                 "@contents": "",
                             },
-                         },
+                        },
                         "notifications": {
                             "sample-notification.xml": {
                                 "@contents": "<?xml version='1.0'>\n<should-be-ignored />"
@@ -353,8 +338,6 @@
         are upgraded to /calendars/__uids__/XX/YY/<guid> form
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -503,8 +486,6 @@
         whose records don't exist are moved into dataroot/archived/
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -575,8 +556,6 @@
         whose records don't exist are moved into dataroot/archived/
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "archived" :
             {
@@ -663,8 +642,6 @@
         interrupt an upgrade.
         """
 
-        self.setUpXMLDirectory()
-
         ignoredUIDContents = {
             "64" : {
                 "23" : {
@@ -757,8 +734,6 @@
         interrupt an upgrade.
         """
 
-        self.setUpXMLDirectory()
-
         beforeUIDContents = {
             "64" : {
                 "23" : {
@@ -867,8 +842,6 @@
         are upgraded to /calendars/__uids__/XX/YY/<guid>/ form
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -978,8 +951,6 @@
         form are upgraded correctly in place
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -1106,8 +1077,6 @@
         form which require no changes are untouched
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -1233,8 +1202,6 @@
         Verify that inbox items older than 60 days are deleted
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -1337,8 +1304,6 @@
         also doesn't write the new version file
         """
 
-        self.setUpXMLDirectory()
-
         before = {
             "calendars" :
             {
@@ -1454,7 +1419,7 @@
         self.setUpInitialStates()
         # Override the normal getResourceInfo method with our own:
         # XMLDirectoryService.getResourceInfo = _getResourceInfo
-        self.patch(XMLDirectoryService, "getResourceInfo", _getResourceInfo)
+        # self.patch(XMLDirectoryService, "getResourceInfo", _getResourceInfo)
 
         before = {
             "trigger_resource_migration" : {
@@ -1520,7 +1485,9 @@
             autoSchedule = autoSchedule == 1
             self.assertEquals(info[0], autoSchedule)
 
+    test_migrateResourceInfo.todo = "Need to port to twext.who"
 
+
     def test_removeIllegalCharacters(self):
         """
         Control characters aside from NL and CR are removed.
@@ -1536,55 +1503,96 @@
         self.assertFalse(changed)
 
 
+    @inlineCallbacks
     def test_normalizeCUAddrs(self):
         """
         Ensure that calendar user addresses (CUAs) are cached so we can
         reduce the number of principal lookup calls during upgrade.
         """
 
-        class StubPrincipal(object):
-            def __init__(self, record):
-                self.record = record
-
         class StubRecord(object):
-            def __init__(self, fullName, guid, cuas):
-                self.fullName = fullName
-                self.guid = guid
+            def __init__(self, fullNames, uid, cuas):
+                self.fullNames = fullNames
+                self.uid = uid
                 self.calendarUserAddresses = cuas
 
+            @property
+            def displayName(self):
+                return self.fullNames[0]
+
         class StubDirectory(object):
             def __init__(self):
                 self.count = 0
 
-            def principalForCalendarUserAddress(self, cuaddr):
+            def recordWithCalendarUserAddress(self, cuaddr):
                 self.count += 1
                 record = records.get(cuaddr, None)
                 if record is not None:
-                    return StubPrincipal(record)
+                    return succeed(record)
                 else:
                     raise Exception
 
         records = {
-            "mailto:a at example.com" :
-                StubRecord("User A", 123, ("mailto:a at example.com", "urn:uuid:123")),
-            "mailto:b at example.com" :
-                StubRecord("User B", 234, ("mailto:b at example.com", "urn:uuid:234")),
-            "/principals/users/a" :
-                StubRecord("User A", 123, ("mailto:a at example.com", "urn:uuid:123")),
-            "/principals/users/b" :
-                StubRecord("User B", 234, ("mailto:b at example.com", "urn:uuid:234")),
+            "mailto:a at example.com":
+                StubRecord(("User A",), u"123", ("mailto:a at example.com", "urn:uuid:123")),
+            "mailto:b at example.com":
+                StubRecord(("User B",), u"234", ("mailto:b at example.com", "urn:uuid:234")),
+            "/principals/users/a":
+                StubRecord(("User A",), u"123", ("mailto:a at example.com", "urn:uuid:123")),
+            "/principals/users/b":
+                StubRecord(("User B",), u"234", ("mailto:b at example.com", "urn:uuid:234")),
         }
 
         directory = StubDirectory()
         cuaCache = {}
-        normalizeCUAddrs(normalizeEvent, directory, cuaCache)
-        normalizeCUAddrs(normalizeEvent, directory, cuaCache)
+        yield normalizeCUAddrs(normalizeEvent, directory, cuaCache)
+        yield normalizeCUAddrs(normalizeEvent, directory, cuaCache)
 
         # Ensure we only called principalForCalendarUserAddress 3 times.  It
         # would have been 8 times without the cuaCache.
         self.assertEquals(directory.count, 3)
 
 
+    @inlineCallbacks
+    def test_migrateDelegates(self):
+        store = self.storeUnderTest()
+        record = yield self.directory.recordWithUID(u"mercury")
+        txn = store.newTransaction()
+        writeDelegates = yield delegatesOf(txn, record, True)
+        self.assertEquals(len(writeDelegates), 0)
+        yield txn.commit()
+
+        # Load delegates from xml into sqlite
+        sqliteProxyService = ProxySqliteDB("proxies.sqlite")
+        proxyFile = os.path.join(config.DataRoot, "proxies.xml")
+        yield loadDelegatesFromXML(proxyFile, sqliteProxyService)
+
+        # Load delegates from sqlite into store
+        yield migrateDelegatesToStore(sqliteProxyService, store)
+
+        # Check delegates in store
+        txn = store.newTransaction()
+        writeDelegates = yield delegatesOf(txn, record, True)
+        self.assertEquals(len(writeDelegates), 1)
+        self.assertEquals(
+            set([d.uid for d in writeDelegates]),
+            set([u"left_coast"])
+        )
+
+        record = yield self.directory.recordWithUID(u"non_calendar_proxy")
+
+        readDelegates = yield delegatesOf(txn, record, False)
+        self.assertEquals(len(readDelegates), 1)
+        self.assertEquals(
+            set([d.uid for d in readDelegates]),
+            set([u"recursive2_coasts"])
+        )
+
+        yield txn.commit()
+
+
+
+
 normalizeEvent = """BEGIN:VCALENDAR
 VERSION:2.0
 BEGIN:VEVENT

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_wrapping.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_wrapping.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/test_wrapping.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -28,11 +28,12 @@
 from txweb2.responsecode import UNAUTHORIZED
 from txweb2.stream import MemoryStream
 
+from twext.who.idirectory import RecordType
+
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.defer import maybeDeferred
 
 from twistedcaldav.config import config
-from twistedcaldav.directory.test.test_xmlfile import XMLFileBase
 from twistedcaldav.ical import Component as VComponent
 from twistedcaldav.storebridge import DropboxCollection, \
     CalendarCollectionResource
@@ -51,11 +52,14 @@
 
 import hashlib
 
+
 def _todo(f, why):
     f.todo = why
     return f
 rewriteOrRemove = lambda f: _todo(f, "Rewrite or remove")
 
+
+
 class FakeChanRequest(object):
     code = 'request-not-finished'
 
@@ -113,7 +117,7 @@
         @param objectText: Some iCalendar text to populate it with.
         @type objectText: str
         """
-        record = self.directory.recordWithShortName("users", "wsanchez")
+        record = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         uid = record.uid
         txn = self.transactionUnderTest()
         home = yield txn.calendarHomeWithUID(uid, True)
@@ -132,7 +136,7 @@
         @param objectText: Some iVcard text to populate it with.
         @type objectText: str
         """
-        record = self.directory.recordWithShortName("users", "wsanchez")
+        record = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         uid = record.uid
         txn = self.transactionUnderTest()
         home = yield txn.addressbookHomeWithUID(uid, True)
@@ -171,9 +175,10 @@
             "http://localhost:8008/" + path
         )
         if user is not None:
-            guid = XMLFileBase.users[user]["guid"]
+            record = yield self.directory.recordWithShortName(RecordType.user, user)
+            uid = record.uid
             req.authnUser = req.authzUser = (
-                davxml.Principal(davxml.HRef('/principals/__uids__/' + guid + '/'))
+                davxml.Principal(davxml.HRef('/principals/__uids__/' + uid + '/'))
             )
         returnValue(aResource)
 
@@ -201,8 +206,10 @@
         Verify that the C{_principalCollections} attribute of the given
         L{Resource} is accurately set.
         """
-        self.assertEquals(resource._principalCollections,
-                          frozenset([self.principalsResource]))
+        self.assertEquals(
+            resource._principalCollections,
+            frozenset([self.actualRoot.getChild("principals")])
+        )
 
 
     @inlineCallbacks
@@ -271,7 +278,7 @@
         )
         yield self.commit()
         self.assertIsInstance(dropBoxResource, DropboxCollection)
-        dropboxHomeType = davxml.ResourceType.dropboxhome #@UndefinedVariable
+        dropboxHomeType = davxml.ResourceType.dropboxhome  # @UndefinedVariable
         self.assertEquals(dropBoxResource.resourceType(),
                           dropboxHomeType)
 
@@ -285,7 +292,7 @@
         C{CalendarHome.calendarWithName}.
         """
         calDavFile = yield self.getResource("calendars/users/wsanchez/calendar")
-        regularCalendarType = davxml.ResourceType.calendar #@UndefinedVariable
+        regularCalendarType = davxml.ResourceType.calendar  # @UndefinedVariable
         self.assertEquals(calDavFile.resourceType(),
                           regularCalendarType)
         yield self.commit()
@@ -344,8 +351,11 @@
             self.assertIdentical(
                 homeChild._associatedTransaction,
                 homeTransaction,
-                "transaction mismatch on %s; %r is not %r " %
-                    (name, homeChild._associatedTransaction, homeTransaction))
+                "transaction mismatch on {n}; {at} is not {ht} ".format(
+                    n=name, at=homeChild._associatedTransaction,
+                    ht=homeTransaction
+                )
+            )
 
 
     @inlineCallbacks
@@ -415,7 +425,7 @@
         Creating a AddressBookHomeProvisioningFile will create a paired
         AddressBookStore.
         """
-        assertProvides(self, IDataStore, self.addressbookCollection._newStore)
+        assertProvides(self, IDataStore, self.actualRoot.getChild("addressbooks")._newStore)
 
 
     @inlineCallbacks
@@ -575,12 +585,13 @@
         yield NamedLock.acquire(txn, "ImplicitUIDLock:%s" % (hashlib.md5("uid1").hexdigest(),))
 
         # PUT fails
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         request = SimpleStoreRequest(
             self,
             "PUT",
             "/calendars/users/wsanchez/calendar/1.ics",
             headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
-            authid="wsanchez"
+            authRecord=authRecord
         )
         request.stream = MemoryStream("""BEGIN:VCALENDAR
 CALSCALE:GREGORIAN
@@ -606,12 +617,13 @@
         """
 
         # PUT works
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         request = SimpleStoreRequest(
             self,
             "PUT",
             "/calendars/users/wsanchez/calendar/1.ics",
             headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
-            authid="wsanchez"
+            authRecord=authRecord
         )
         request.stream = MemoryStream("""BEGIN:VCALENDAR
 CALSCALE:GREGORIAN
@@ -635,11 +647,12 @@
         txn = self.transactionUnderTest()
         yield NamedLock.acquire(txn, "ImplicitUIDLock:%s" % (hashlib.md5("uid1").hexdigest(),))
 
+        authRecord = yield self.directory.recordWithShortName(RecordType.user, u"wsanchez")
         request = SimpleStoreRequest(
             self,
             "DELETE",
             "/calendars/users/wsanchez/calendar/1.ics",
-            authid="wsanchez"
+            authRecord=authRecord
         )
         response = yield self.send(request)
         self.assertEqual(response.code, responsecode.SERVICE_UNAVAILABLE)

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/test/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -17,45 +17,36 @@
 from __future__ import with_statement
 
 import os
-import xattr
 
-from twistedcaldav.stdconfig import config
-
-from twisted.python.failure import Failure
+from calendarserver.provision.root import RootResource
+from calendarserver.tap.util import getRootResource
+from twext.python.filepath import CachingFilePath as FilePath
+from twext.python.log import Logger
 from twisted.internet.base import DelayedCall
 from twisted.internet.defer import succeed, fail, inlineCallbacks, returnValue
 from twisted.internet.protocol import ProcessProtocol
-
-from twext.python.filepath import CachingFilePath as FilePath
-import txweb2.dav.test.util
-from txdav.xml import element as davxml, element
-from txweb2.http import HTTPError, StatusResponse
-
+from twisted.python.failure import Failure
 from twistedcaldav import memcacher
-from twistedcaldav.memcacheclient import ClientFactory
 from twistedcaldav.bind import doBind
-from twistedcaldav.directory import augment
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
 from twistedcaldav.directory.calendar import (
     DirectoryCalendarHomeProvisioningResource
 )
-from twistedcaldav.directory.principal import (
-    DirectoryPrincipalProvisioningResource)
-from twistedcaldav.directory.aggregate import AggregateDirectoryService
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
-
-from txdav.common.datastore.test.util import deriveQuota, CommonCommonTests
-from txdav.common.datastore.file import CommonDataStore
-
-from calendarserver.provision.root import RootResource
-
-from twext.python.log import Logger
+from twistedcaldav.directory.util import transactionFromRequest
+from twistedcaldav.memcacheclient import ClientFactory
+from twistedcaldav.stdconfig import config
 from txdav.caldav.datastore.test.util import buildCalendarStore
-from calendarserver.tap.util import getRootResource, directoryFromConfig
+from txdav.common.datastore.file import CommonDataStore
+from txdav.common.datastore.test.util import deriveQuota, CommonCommonTests
+from txdav.who.util import directoryFromConfig
+from txdav.xml import element as davxml, element
 from txweb2.dav.test.util import SimpleRequest
-from twistedcaldav.directory.util import transactionFromRequest
-from twistedcaldav.directory.directory import DirectoryService
+import txweb2.dav.test.util
+from txweb2.http import HTTPError, StatusResponse
+import xattr
+from txweb2.server import Site
 
+
 log = Logger()
 
 
@@ -67,10 +58,12 @@
 ]
 DelayedCall.debug = True
 
+
 def _todo(f, why):
     f.todo = why
     return f
 
+
 featureUnimplemented = lambda f: _todo(f, "Feature unimplemented")
 testUnimplemented = lambda f: _todo(f, "Test unimplemented")
 todo = lambda why: lambda f: _todo(f, why)
@@ -78,97 +71,27 @@
 dirTest = FilePath(__file__).parent().sibling("directory").child("test")
 
 xmlFile = dirTest.child("accounts.xml")
+resourcesFile = dirTest.child("resources.xml")
 augmentsFile = dirTest.child("augments.xml")
 proxiesFile = dirTest.child("proxies.xml")
 
 
 
-class DirectoryFixture(object):
-    """
-    Test fixture for creating various parts of the resource hierarchy related
-    to directories.
-    """
 
-    def __init__(self):
-        def _setUpPrincipals(ds):
-            # FIXME: see FIXME in
-            # DirectoryPrincipalProvisioningResource.__init__; this performs a
-            # necessary modification to any directory service object for it to
-            # be fully functional.
-            self.principalsResource = DirectoryPrincipalProvisioningResource(
-                "/principals/", ds
-            )
-        self._directoryChangeHooks = [_setUpPrincipals]
 
-    directoryService = None
-    principalsResource = None
-
-    def addDirectoryService(self, newService):
-        """
-        Add an L{IDirectoryService} to this test case.
-
-        If this test case does not have a directory service yet, create it and
-        assign C{directoryService} and C{principalsResource} attributes to this
-        test case.
-
-        If the test case already has a directory service, create an
-        L{AggregateDirectoryService} and re-assign the C{self.directoryService}
-        attribute to point at it instead, while setting the C{realmName} of the
-        new service to match the old one.
-
-        If the test already has an L{AggregateDirectoryService}, create a
-        I{new} L{AggregateDirectoryService} with the same list of services,
-        after adjusting the new service's realm to match the existing ones.
-        """
-
-        if self.directoryService is None:
-            directoryService = newService
-        else:
-            newService.realmName = self.directoryService.realmName
-            if isinstance(self.directoryService, AggregateDirectoryService):
-                directories = set(self.directoryService._recordTypes.items())
-                directories.add(newService)
-            else:
-                directories = [newService, self.directoryService]
-            directoryService = AggregateDirectoryService(directories, None)
-
-        self.directoryService = directoryService
-        # FIXME: see FIXME in DirectoryPrincipalProvisioningResource.__init__;
-        # this performs a necessary modification to the directory service object
-        # for it to be fully functional.
-        for hook in self._directoryChangeHooks:
-            hook(directoryService)
-
-
-    def whenDirectoryServiceChanges(self, callback):
-        """
-        When the C{directoryService} attribute is changed by
-        L{TestCase.addDirectoryService}, call the given callback in order to
-        update any state which relies upon that service.
-
-        If there's already a directory, invoke the callback immediately.
-        """
-        self._directoryChangeHooks.append(callback)
-        if self.directoryService is not None:
-            callback(self.directoryService)
-
-
-
 class SimpleStoreRequest(SimpleRequest):
     """
     A SimpleRequest that automatically grabs the proper transaction for a test.
     """
-    def __init__(self, test, method, uri, headers=None, content=None, authid=None):
+    def __init__(self, test, method, uri, headers=None, content=None, authRecord=None):
         super(SimpleStoreRequest, self).__init__(test.site, method, uri, headers, content)
         self._test = test
         self._newStoreTransaction = test.transactionUnderTest(txn=transactionFromRequest(self, test.storeUnderTest()))
         self.credentialFactories = {}
 
         # Fake credentials if auth needed
-        if authid is not None:
-            record = self._test.directory.recordWithShortName(DirectoryService.recordType_users, authid)
-            if record:
-                self.authzUser = self.authnUser = element.Principal(element.HRef("/principals/__uids__/%s/" % (record.uid,)))
+        if authRecord is not None:
+            self.authzUser = self.authnUser = element.Principal(element.HRef("/principals/__uids__/%s/" % (authRecord.uid,)))
 
 
     @inlineCallbacks
@@ -198,17 +121,16 @@
 
         self.configure()
 
-        self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory, directoryFromConfig(config))
+        self._sqlCalendarStore = yield buildCalendarStore(
+            self, self.notifierFactory, None
+        )
+        self.directory = directoryFromConfig(config, self._sqlCalendarStore)
+        self._sqlCalendarStore.setDirectoryService(self.directory)
+
         self.rootResource = getRootResource(config, self._sqlCalendarStore)
-        self.directory = self._sqlCalendarStore.directoryService()
+        self.actualRoot = self.rootResource.resource.resource
+        self.site = Site(self.actualRoot)
 
-        self.principalsResource = DirectoryPrincipalProvisioningResource("/principals/", self.directory)
-        self.site.resource.putChild("principals", self.principalsResource)
-        self.calendarCollection = DirectoryCalendarHomeProvisioningResource(self.directory, "/calendars/", self._sqlCalendarStore)
-        self.site.resource.putChild("calendars", self.calendarCollection)
-        self.addressbookCollection = DirectoryAddressBookHomeProvisioningResource(self.directory, "/addressbooks/", self._sqlCalendarStore)
-        self.site.resource.putChild("addressbooks", self.addressbookCollection)
-
         yield self.populate()
 
 
@@ -258,123 +180,17 @@
         accounts = FilePath(config.DataRoot).child("accounts.xml")
         accounts.setContent(xmlFile.getContent())
 
+        resources = FilePath(config.DataRoot).child("resources.xml")
+        resources.setContent(resourcesFile.getContent())
 
-    @property
-    def directoryService(self):
-        """
-        Read-only alias for L{DirectoryFixture.directoryService} for
-        compatibility with older tests.  TODO: remove this.
-        """
-        return self.directory
+        augments = FilePath(config.DataRoot).child("augments.xml")
+        augments.setContent(augmentsFile.getContent())
 
+        proxies = FilePath(config.DataRoot).child("proxies.xml")
+        proxies.setContent(proxiesFile.getContent())
 
 
-class TestCase(txweb2.dav.test.util.TestCase):
-    resource_class = RootResource
 
-    def createDataStore(self):
-        """
-        Create an L{IDataStore} that can store calendars (but not
-        addressbooks.)  By default returns a L{CommonDataStore}, but this is a
-        hook for subclasses to override to provide different data stores.
-        """
-        return CommonDataStore(FilePath(config.DocumentRoot), None, None, True, False,
-                               quota=deriveQuota(self))
-
-
-    def createStockDirectoryService(self):
-        """
-        Create a stock C{directoryService} attribute and assign it.
-        """
-        self.xmlFile = FilePath(config.DataRoot).child("accounts.xml")
-        self.xmlFile.setContent(xmlFile.getContent())
-        self.directoryFixture.addDirectoryService(XMLDirectoryService({
-            "xmlFile": "accounts.xml",
-            "augmentService":
-                augment.AugmentXMLDB(xmlFiles=(augmentsFile.path,)),
-        }))
-
-
-    def setupCalendars(self):
-        """
-        When a directory service exists, set up the resources at C{/calendars}
-        and C{/addressbooks} (a L{DirectoryCalendarHomeProvisioningResource}
-        and L{DirectoryAddressBookHomeProvisioningResource} respectively), and
-        assign them to the C{self.calendarCollection} and
-        C{self.addressbookCollection} attributes.
-
-        A directory service may be associated with this L{TestCase} with
-        L{TestCase.createStockDirectoryService} or
-        L{TestCase.directoryFixture.addDirectoryService}.
-        """
-        newStore = self.createDataStore()
-        @self.directoryFixture.whenDirectoryServiceChanges
-        def putAllChildren(ds):
-            self.calendarCollection = (
-                DirectoryCalendarHomeProvisioningResource(
-                    ds, "/calendars/", newStore
-                ))
-            self.site.resource.putChild("calendars", self.calendarCollection)
-            self.addressbookCollection = (
-                DirectoryAddressBookHomeProvisioningResource(
-                    ds, "/addressbooks/", newStore
-                ))
-            self.site.resource.putChild("addressbooks",
-                                        self.addressbookCollection)
-
-
-    def configure(self):
-        """
-        Adjust the global configuration for this test.
-        """
-        config.reset()
-
-        config.ServerRoot = os.path.abspath(self.serverRoot)
-        config.ConfigRoot = "config"
-        config.LogRoot = "logs"
-        config.RunRoot = "logs"
-
-        config.Memcached.Pools.Default.ClientEnabled = False
-        config.Memcached.Pools.Default.ServerEnabled = False
-        ClientFactory.allowTestCache = True
-        memcacher.Memcacher.allowTestCache = True
-        memcacher.Memcacher.memoryCacheInstance = None
-        config.DirectoryAddressBook.Enabled = False
-        config.UsePackageTimezones = True
-
-
-    @property
-    def directoryService(self):
-        """
-        Read-only alias for L{DirectoryFixture.directoryService} for
-        compatibility with older tests.  TODO: remove this.
-        """
-        return self.directoryFixture.directoryService
-
-
-    def setUp(self):
-        super(TestCase, self).setUp()
-
-        self.directoryFixture = DirectoryFixture()
-
-        # FIXME: this is only here to workaround circular imports
-        doBind()
-
-        self.serverRoot = self.mktemp()
-        os.mkdir(self.serverRoot)
-
-        self.configure()
-
-        if not os.path.exists(config.DataRoot):
-            os.makedirs(config.DataRoot)
-        if not os.path.exists(config.DocumentRoot):
-            os.makedirs(config.DocumentRoot)
-        if not os.path.exists(config.ConfigRoot):
-            os.makedirs(config.ConfigRoot)
-        if not os.path.exists(config.LogRoot):
-            os.makedirs(config.LogRoot)
-
-
     def createHierarchy(self, structure, root=None):
         if root is None:
             root = os.path.abspath(self.mktemp())
@@ -506,7 +322,7 @@
                                     print("Xattr mismatch:", childPath, attr)
                                     print((xattr.getxattr(childPath, attr), " != ", value))
                                     return False
-                            else: # method
+                            else:  # method
                                 if not value(xattr.getxattr(childPath, attr)):
                                     return False
 
@@ -528,6 +344,93 @@
 
 
 
+class TestCase(txweb2.dav.test.util.TestCase):
+    resource_class = RootResource
+
+    def createDataStore(self):
+        """
+        Create an L{IDataStore} that can store calendars (but not
+        addressbooks.)  By default returns a L{CommonDataStore}, but this is a
+        hook for subclasses to override to provide different data stores.
+        """
+        return CommonDataStore(FilePath(config.DocumentRoot), None, None, True, False,
+                               quota=deriveQuota(self))
+
+
+    def setupCalendars(self):
+        """
+        When a directory service exists, set up the resources at C{/calendars}
+        and C{/addressbooks} (a L{DirectoryCalendarHomeProvisioningResource}
+        and L{DirectoryAddressBookHomeProvisioningResource} respectively), and
+        assign them to the C{self.calendarCollection} and
+        C{self.addressbookCollection} attributes.
+
+        A directory service may be associated with this L{TestCase} with
+        L{TestCase.createStockDirectoryService} or
+        L{TestCase.directoryFixture.addDirectoryService}.
+        """
+        newStore = self.createDataStore()
+
+
+        @self.directoryFixture.whenDirectoryServiceChanges
+        def putAllChildren(ds):
+            self.calendarCollection = (
+                DirectoryCalendarHomeProvisioningResource(
+                    ds, "/calendars/", newStore
+                ))
+            self.site.resource.putChild("calendars", self.calendarCollection)
+            self.addressbookCollection = (
+                DirectoryAddressBookHomeProvisioningResource(
+                    ds, "/addressbooks/", newStore
+                ))
+            self.site.resource.putChild("addressbooks",
+                                        self.addressbookCollection)
+
+
+    def configure(self):
+        """
+        Adjust the global configuration for this test.
+        """
+        config.reset()
+
+        config.ServerRoot = os.path.abspath(self.serverRoot)
+        config.ConfigRoot = "config"
+        config.LogRoot = "logs"
+        config.RunRoot = "logs"
+
+        config.Memcached.Pools.Default.ClientEnabled = False
+        config.Memcached.Pools.Default.ServerEnabled = False
+        ClientFactory.allowTestCache = True
+        memcacher.Memcacher.allowTestCache = True
+        memcacher.Memcacher.memoryCacheInstance = None
+        config.DirectoryAddressBook.Enabled = False
+        config.UsePackageTimezones = True
+
+
+
+    def setUp(self):
+        super(TestCase, self).setUp()
+
+        # FIXME: this is only here to workaround circular imports
+        doBind()
+
+        self.serverRoot = self.mktemp()
+        os.mkdir(self.serverRoot)
+
+        self.configure()
+
+        if not os.path.exists(config.DataRoot):
+            os.makedirs(config.DataRoot)
+        if not os.path.exists(config.DocumentRoot):
+            os.makedirs(config.DocumentRoot)
+        if not os.path.exists(config.ConfigRoot):
+            os.makedirs(config.ConfigRoot)
+        if not os.path.exists(config.LogRoot):
+            os.makedirs(config.LogRoot)
+
+
+
+
 class norequest(object):
     def addResponseFilter(self, filter):
         "stub; ignore me"
@@ -555,7 +458,8 @@
         that stores the data for that L{CalendarHomeResource}.
         """
         super(HomeTestCase, self).setUp()
-        self.createStockDirectoryService()
+
+
         @self.directoryFixture.whenDirectoryServiceChanges
         def addHomeProvisioner(ds):
             self.homeProvisioner = DirectoryCalendarHomeProvisioningResource(
@@ -639,7 +543,8 @@
         file.
         """
         super(AddressBookHomeTestCase, self).setUp()
-        self.createStockDirectoryService()
+
+
         @self.directoryFixture.whenDirectoryServiceChanges
         def addHomeProvisioner(ds):
             self.homeProvisioner = DirectoryAddressBookHomeProvisioningResource(

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezoneservice.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezoneservice.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezoneservice.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -86,15 +86,17 @@
 
 
     def defaultAccessControlList(self):
-        return davxml.ACL(
-            # DAV:Read for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(
-                    davxml.Privilege(davxml.Read()),
+        return succeed(
+            davxml.ACL(
+                # DAV:Read for all principals (includes anonymous)
+                davxml.ACE(
+                    davxml.Principal(davxml.All()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                    ),
+                    davxml.Protected(),
                 ),
-                davxml.Protected(),
-            ),
+            )
         )
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezonestdservice.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezonestdservice.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/timezonestdservice.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -146,15 +146,17 @@
 
 
     def defaultAccessControlList(self):
-        return davxml.ACL(
-            # DAV:Read for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(
-                    davxml.Privilege(davxml.Read()),
+        return succeed(
+            davxml.ACL(
+                # DAV:Read for all principals (includes anonymous)
+                davxml.ACE(
+                    davxml.Principal(davxml.All()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                    ),
+                    davxml.Protected(),
                 ),
-                davxml.Protected(),
-            ),
+            )
         )
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/upgrade.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/upgrade.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/upgrade.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -26,7 +26,6 @@
 import grp
 import shutil
 import errno
-import operator
 import time
 from zlib import compress
 from cPickle import loads as unpickle, UnpicklingError
@@ -37,18 +36,14 @@
 from txweb2.dav.fileop import rmdir
 
 from twistedcaldav import caldavxml
-from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.calendaruserproxyloader import XMLCalendarUserProxyLoader
-from twistedcaldav.directory.directory import DirectoryService
-from twistedcaldav.directory.directory import GroupMembershipCacheUpdater
 from twistedcaldav.directory.principal import DirectoryCalendarPrincipalResource
 from twistedcaldav.directory.resourceinfo import ResourceInfoDatabase
-from twistedcaldav.directory.xmlfile import XMLDirectoryService
 from twistedcaldav.ical import Component
 from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
 from txdav.caldav.datastore.scheduling.imip.mailgateway import MailGatewayTokensDatabase
 from txdav.caldav.datastore.scheduling.scheduler import DirectScheduler
-from twistedcaldav.util import normalizationLookup
+from txdav.caldav.datastore.util import normalizationLookup
 
 from twisted.internet.defer import (
     inlineCallbacks, succeed, returnValue
@@ -58,13 +53,18 @@
 
 from txdav.caldav.datastore.index_file import db_basename
 
-from twisted.protocols.amp import AMP, Command, String, Boolean
+# from twisted.protocols.amp import AMP, Command, String, Boolean
 
-from calendarserver.tap.util import getRootResource, FakeRequest, directoryFromConfig
-from calendarserver.tools.util import getDirectory
+from calendarserver.tap.util import getRootResource, FakeRequest
 
 from txdav.caldav.datastore.scheduling.imip.mailgateway import migrateTokensToStore
 
+from twext.who.idirectory import RecordType
+from txdav.who.idirectory import RecordType as CalRecordType
+from txdav.who.delegates import addDelegate
+from twistedcaldav.directory.calendaruserproxy import ProxySqliteDB
+
+
 deadPropertyXattrPrefix = namedAny(
     "txdav.base.propertystore.xattr.PropertyStore.deadPropertyXattrPrefix"
 )
@@ -74,6 +74,7 @@
 
 log = Logger()
 
+
 def xattrname(n):
     return deadPropertyXattrPrefix + n
 
@@ -121,6 +122,7 @@
 
 
 
+ at inlineCallbacks
 def upgradeCalendarCollection(calPath, directory, cuaCache):
     errorOccurred = False
     collectionUpdated = False
@@ -147,8 +149,10 @@
                     log.warn("Fixing bad quotes in %s" % (resPath,))
                     needsRewrite = True
             except Exception, e:
-                log.error("Error while fixing bad quotes in %s: %s" %
-                    (resPath, e))
+                log.error(
+                    "Error while fixing bad quotes in %s: %s" %
+                    (resPath, e)
+                )
                 errorOccurred = True
                 continue
 
@@ -158,19 +162,23 @@
                     log.warn("Removing illegal characters in %s" % (resPath,))
                     needsRewrite = True
             except Exception, e:
-                log.error("Error while removing illegal characters in %s: %s" %
-                    (resPath, e))
+                log.error(
+                    "Error while removing illegal characters in %s: %s" %
+                    (resPath, e)
+                )
                 errorOccurred = True
                 continue
 
             try:
-                data, fixed = normalizeCUAddrs(data, directory, cuaCache)
+                data, fixed = (yield normalizeCUAddrs(data, directory, cuaCache))
                 if fixed:
                     log.debug("Normalized CUAddrs in %s" % (resPath,))
                     needsRewrite = True
             except Exception, e:
-                log.error("Error while normalizing %s: %s" %
-                    (resPath, e))
+                log.error(
+                    "Error while normalizing %s: %s" %
+                    (resPath, e)
+                )
                 errorOccurred = True
                 continue
 
@@ -207,10 +215,11 @@
         except:
             raise
 
-    return errorOccurred
+    returnValue(errorOccurred)
 
 
 
+ at inlineCallbacks
 def upgradeCalendarHome(homePath, directory, cuaCache):
 
     errorOccurred = False
@@ -229,7 +238,7 @@
                 rmdir(calPath)
                 continue
             log.debug("Upgrading calendar: %s" % (calPath,))
-            if not upgradeCalendarCollection(calPath, directory, cuaCache):
+            if not (yield upgradeCalendarCollection(calPath, directory, cuaCache)):
                 errorOccurred = True
 
             # Change the calendar-free-busy-set xattrs of the inbox to the
@@ -238,7 +247,7 @@
                 try:
                     for attr, value in xattr.xattr(calPath).iteritems():
                         if attr == xattrname("{urn:ietf:params:xml:ns:caldav}calendar-free-busy-set"):
-                            value = updateFreeBusySet(value, directory)
+                            value = yield updateFreeBusySet(value, directory)
                             if value is not None:
                                 # Need to write the xattr back to disk
                                 xattr.setxattr(calPath, attr, value)
@@ -254,43 +263,44 @@
         log.error("Failed to upgrade calendar home %s: %s" % (homePath, e))
         raise
 
-    return errorOccurred
+    returnValue(errorOccurred)
 
 
 
-class UpgradeOneHome(Command):
-    arguments = [('path', String())]
-    response = [('succeeded', Boolean())]
+# class UpgradeOneHome(Command):
+#     arguments = [('path', String())]
+#     response = [('succeeded', Boolean())]
 
 
 
-class To1Driver(AMP):
-    """
-    Upgrade driver which runs in the parent process.
-    """
+# class To1Driver(AMP):
+#     """
+#     Upgrade driver which runs in the parent process.
+#     """
 
-    def upgradeHomeInHelper(self, path):
-        return self.callRemote(UpgradeOneHome, path=path).addCallback(
-            operator.itemgetter("succeeded")
-        )
+#     def upgradeHomeInHelper(self, path):
+#         return self.callRemote(UpgradeOneHome, path=path).addCallback(
+#             operator.itemgetter("succeeded")
+#         )
 
 
 
-class To1Home(AMP):
-    """
-    Upgrade worker which runs in dedicated subprocesses.
-    """
+# class To1Home(AMP):
+#     """
+#     Upgrade worker which runs in dedicated subprocesses.
+#     """
 
-    def __init__(self, config):
-        super(To1Home, self).__init__()
-        self.directory = getDirectory(config)
-        self.cuaCache = {}
+#     def __init__(self, config):
+#         super(To1Home, self).__init__()
+#         self.directory = getDirectory(config)
+#         self.cuaCache = {}
 
 
-    @UpgradeOneHome.responder
-    def upgradeOne(self, path):
-        result = upgradeCalendarHome(path, self.directory, self.cuaCache)
-        return dict(succeeded=result)
+#     @UpgradeOneHome.responder
+#     @inlineCallbacks
+#     def upgradeOne(self, path):
+#         result = yield upgradeCalendarHome(path, self.directory, self.cuaCache)
+#         returnValue(dict(succeeded=result))
 
 
 
@@ -300,13 +310,14 @@
     Upconvert data from any calendar server version prior to data format 1.
     """
     errorOccurred = []
+
     def setError(f=None):
         if f is not None:
             log.error(f)
         errorOccurred.append(True)
 
 
-    def doProxyDatabaseMoveUpgrade(config, uid= -1, gid= -1):
+    def doProxyDatabaseMoveUpgrade(config, uid=-1, gid=-1):
         # See if the new one is already present
         oldFilename = ".db.calendaruserproxy"
         newFilename = "proxies.sqlite"
@@ -345,7 +356,7 @@
         )
 
 
-    def moveCalendarHome(oldHome, newHome, uid= -1, gid= -1):
+    def moveCalendarHome(oldHome, newHome, uid=-1, gid=-1):
         if os.path.exists(newHome):
             # Both old and new homes exist; stop immediately to let the
             # administrator fix it
@@ -354,8 +365,9 @@
                 % (oldHome, newHome)
             )
 
-        makeDirsUserGroup(os.path.dirname(newHome.rstrip("/")), uid=uid,
-            gid=gid)
+        makeDirsUserGroup(
+            os.path.dirname(newHome.rstrip("/")), uid=uid, gid=gid
+        )
         os.rename(oldHome, newHome)
 
 
@@ -366,12 +378,15 @@
         service, because in "v1" that's where this info lived.
         """
 
+        print("FIXME, need to port migrateResourceInfo to twext.who")
+        returnValue(None)
+
         log.warn("Fetching delegate assignments and auto-schedule settings from directory")
         resourceInfo = directory.getResourceInfo()
         if len(resourceInfo) == 0:
             # Nothing to migrate, or else not appleopendirectory
             log.warn("No resource info found in directory")
-            return
+            returnValue(None)
 
         log.warn("Found info for %d resources and locations in directory; applying settings" % (len(resourceInfo),))
 
@@ -467,20 +482,23 @@
                 os.chown(uidHomes, uid, gid)
 
             for recordType, dirName in (
-                (DirectoryService.recordType_users, "users"),
-                (DirectoryService.recordType_groups, "groups"),
-                (DirectoryService.recordType_locations, "locations"),
-                (DirectoryService.recordType_resources, "resources"),
+                (RecordType.user, u"users"),
+                (RecordType.group, u"groups"),
+                (CalRecordType.location, u"locations"),
+                (CalRecordType.resource, u"resources"),
             ):
                 dirPath = os.path.join(calRoot, dirName)
                 if os.path.exists(dirPath):
                     for shortName in os.listdir(dirPath):
-                        record = directory.recordWithShortName(recordType,
-                            shortName)
+                        record = yield directory.recordWithShortName(
+                            recordType, shortName
+                        )
                         oldHome = os.path.join(dirPath, shortName)
                         if record is not None:
-                            newHome = os.path.join(uidHomes, record.uid[0:2],
-                                record.uid[2:4], record.uid)
+                            newHome = os.path.join(
+                                uidHomes, record.uid[0:2],
+                                record.uid[2:4], record.uid
+                            )
                             moveCalendarHome(oldHome, newHome, uid=uid, gid=gid)
                         else:
                             # an orphaned calendar home (principal no longer
@@ -543,15 +561,17 @@
                                         # Skip non-directories
                                         continue
 
-                                    if not upgradeCalendarHome(
+                                    if not (yield upgradeCalendarHome(
                                         homePath, directory, cuaCache
-                                    ):
+                                    )):
                                         setError()
 
                                     count += 1
                                     if count % 10 == 0:
-                                        log.warn("Processed calendar home %d of %d"
-                                            % (count, total))
+                                        log.warn(
+                                            "Processed calendar home %d of %d"
+                                            % (count, total)
+                                        )
                 log.warn("Done processing calendar homes")
 
     triggerPath = os.path.join(config.ServerRoot, TRIGGER_FILE)
@@ -564,6 +584,7 @@
 
 
 
+ at inlineCallbacks
 def normalizeCUAddrs(data, directory, cuaCache):
     """
     Normalize calendar user addresses to urn:uuid: form.
@@ -583,23 +604,26 @@
     """
     cal = Component.fromString(data)
 
-    def lookupFunction(cuaddr, principalFunction, config):
+    @inlineCallbacks
+    def lookupFunction(cuaddr, recordFunction, config):
 
         # Return cached results, if any.
         if cuaddr in cuaCache:
-            return cuaCache[cuaddr]
+            returnValue(cuaCache[cuaddr])
 
-        result = normalizationLookup(cuaddr, principalFunction, config)
+        result = yield normalizationLookup(cuaddr, recordFunction, config)
 
         # Cache the result
         cuaCache[cuaddr] = result
-        return result
+        returnValue(result)
 
-    cal.normalizeCalendarUserAddresses(lookupFunction,
-        directory.principalForCalendarUserAddress)
+    yield cal.normalizeCalendarUserAddresses(
+        lookupFunction,
+        directory.recordWithCalendarUserAddress
+    )
 
     newData = str(cal)
-    return newData, not newData == data
+    returnValue((newData, not newData == data))
 
 
 
@@ -708,6 +732,7 @@
         raise UpgradeError("Data upgrade failed, see error.log for details")
 
 
+
 # The on-disk version number (which defaults to zero if .calendarserver_version
 # doesn't exist), is compared with each of the numbers in the upgradeMethods
 # array.  If it is less than the number, the associated method is called.
@@ -717,17 +742,17 @@
     (2, upgrade_to_2),
 ]
 
+
 @inlineCallbacks
-def upgradeData(config):
+def upgradeData(config, directory):
 
-    directory = getDirectory()
 
     triggerPath = os.path.join(config.ServerRoot, TRIGGER_FILE)
     if os.path.exists(triggerPath):
         try:
             # Migrate locations/resources now because upgrade_to_1 depends
             # on them being in resources.xml
-            (yield migrateFromOD(config, directory))
+            yield migrateFromOD(config, directory)
         except Exception, e:
             raise UpgradeError("Unable to migrate locations and resources from OD: %s" % (e,))
 
@@ -745,11 +770,15 @@
             with open(versionFilePath) as versionFile:
                 onDiskVersion = int(versionFile.read().strip())
         except IOError:
-            log.error("Cannot open %s; skipping migration" %
-                (versionFilePath,))
+            log.error(
+                "Cannot open %s; skipping migration" %
+                (versionFilePath,)
+            )
         except ValueError:
-            log.error("Invalid version number in %s; skipping migration" %
-                (versionFilePath,))
+            log.error(
+                "Invalid version number in %s; skipping migration" %
+                (versionFilePath,)
+            )
 
     uid, gid = getCalendarServerIDs(config)
 
@@ -779,26 +808,28 @@
 #
 # Utility functions
 #
+ at inlineCallbacks
 def updateFreeBusyHref(href, directory):
     pieces = href.split("/")
     if pieces[2] == "__uids__":
         # Already updated
-        return None
+        returnValue(None)
 
     recordType = pieces[2]
     shortName = pieces[3]
-    record = directory.recordWithShortName(recordType, shortName)
+    record = yield directory.recordWithShortName(recordType, shortName)
     if record is None:
         # We will simply ignore this and not write out an fb-set entry
         log.error("Can't update free-busy href; %s is not in the directory" % shortName)
-        return ""
+        returnValue("")
 
     uid = record.uid
     newHref = "/calendars/__uids__/%s/%s/" % (uid, pieces[4])
-    return newHref
+    returnValue(newHref)
 
 
 
+ at inlineCallbacks
 def updateFreeBusySet(value, directory):
 
     try:
@@ -815,14 +846,13 @@
             freeBusySet = unpickle(value)
         except UnpicklingError:
             log.error("Invalid free/busy property value")
-            # MOR: continue on?
-            return None
+            returnValue(None)
 
     fbset = set()
     didUpdate = False
     for href in freeBusySet.children:
         href = str(href)
-        newHref = updateFreeBusyHref(href, directory)
+        newHref = yield updateFreeBusyHref(href, directory)
         if newHref is None:
             fbset.add(href)
         else:
@@ -831,18 +861,19 @@
                 fbset.add(newHref)
 
     if didUpdate:
-        property = caldavxml.CalendarFreeBusySet(*[element.HRef(href)
-            for href in fbset])
+        property = caldavxml.CalendarFreeBusySet(
+            *[element.HRef(href) for href in fbset]
+        )
         value = compress(property.toxml())
-        return value
+        returnValue(value)
 
-    return None # no update required
+    returnValue(None)  # no update required
 
 
 
-def makeDirsUserGroup(path, uid= -1, gid= -1):
+def makeDirsUserGroup(path, uid=-1, gid=-1):
     parts = path.split("/")
-    if parts[0] == "": # absolute path
+    if parts[0] == "":  # absolute path
         parts[0] = "/"
 
     path = ""
@@ -887,6 +918,8 @@
 
 
 DELETECHARS = ''.join(chr(i) for i in xrange(32) if i not in (9, 10, 13))
+
+
 def removeIllegalCharacters(data):
     """
     Remove all characters below ASCII 32 except HTAB, LF and CR
@@ -904,31 +937,34 @@
 
 
 
-# Deferred
+# # Deferred
 def migrateFromOD(config, directory):
-    #
-    # Migrates locations and resources from OD
-    #
-    try:
-        from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
-        from calendarserver.tools.resources import migrateResources
-    except ImportError:
-        return succeed(None)
+    # FIXME:
+    print("STILL NEED TO IMPLEMENT migrateFromOD")
+    return succeed(None)
+#     #
+#     # Migrates locations and resources from OD
+#     #
+#     try:
+#         from twistedcaldav.directory.appleopendirectory import OpenDirectoryService
+#         from calendarserver.tools.resources import migrateResources
+#     except ImportError:
+#         return succeed(None)
 
-    log.warn("Migrating locations and resources")
+#     log.warn("Migrating locations and resources")
 
-    userService = directory.serviceForRecordType("users")
-    resourceService = directory.serviceForRecordType("resources")
-    if (
-        not isinstance(userService, OpenDirectoryService) or
-        not isinstance(resourceService, XMLDirectoryService)
-    ):
-        # Configuration requires no migration
-        return succeed(None)
+#     userService = directory.serviceForRecordType("users")
+#     resourceService = directory.serviceForRecordType("resources")
+#     if (
+#         not isinstance(userService, OpenDirectoryService) or
+#         not isinstance(resourceService, XMLDirectoryService)
+#     ):
+#         # Configuration requires no migration
+#         return succeed(None)
 
-    # Create internal copies of resources and locations based on what is
-    # found in OD
-    return migrateResources(userService, resourceService)
+#     # Create internal copies of resources and locations based on what is
+#     # found in OD
+#     return migrateResources(userService, resourceService)
 
 
 
@@ -936,7 +972,14 @@
 def migrateAutoSchedule(config, directory):
     # Fetch the autoSchedule assignments from resourceinfo.sqlite and store
     # the values in augments
-    augmentService = directory.augmentService
+    augmentService = None
+    if config.AugmentService.type:
+        augmentClass = namedClass(config.AugmentService.type)
+        try:
+            augmentService = augmentClass(**config.AugmentService.params)
+        except:
+            log.error("Could not start augment service")
+
     if augmentService:
         augmentRecords = []
         dbPath = os.path.join(config.DataRoot, ResourceInfoDatabase.dbFilename)
@@ -946,11 +989,18 @@
             results = resourceInfoDatabase._db_execute(
                 "select GUID, AUTOSCHEDULE from RESOURCEINFO"
             )
-            for guid, autoSchedule in results:
-                record = directory.recordWithGUID(guid)
+            for uid, autoSchedule in results:
+                record = yield directory.recordWithUID(uid)
                 if record is not None:
-                    augmentRecord = (yield augmentService.getAugmentRecord(guid, record.recordType))
-                    augmentRecord.autoSchedule = autoSchedule
+                    augmentRecord = (
+                        yield augmentService.getAugmentRecord(
+                            uid,
+                            directory.recordTypeToOldName(record.recordType)
+                        )
+                    )
+                    augmentRecord.autoScheduleMode = (
+                        "automatic" if autoSchedule else "default"
+                    )
                     augmentRecords.append(augmentRecord)
 
             if augmentRecords:
@@ -958,17 +1008,50 @@
             log.warn("Migrated %d auto-schedule settings" % (len(augmentRecords),))
 
 
+def loadDelegatesFromXML(xmlFile, service):
+    loader = XMLCalendarUserProxyLoader(xmlFile, service)
+    return loader.updateProxyDB()
 
+
+ at inlineCallbacks
+def migrateDelegatesToStore(service, store):
+    directory = store.directoryService()
+    txn = store.newTransaction()
+    for groupName, memberUID in (
+        yield service.query(
+            "select GROUPNAME, MEMBER from GROUPS"
+        )
+    ):
+        if "#" not in groupName:
+            continue
+
+        delegatorUID, groupType = groupName.split("#")
+        delegatorRecord = yield directory.recordWithUID(delegatorUID)
+        if delegatorRecord is None:
+            continue
+
+        delegateRecord = yield directory.recordWithUID(memberUID)
+        if delegateRecord is None:
+            continue
+
+        readWrite = (groupType == "calendar-proxy-write")
+        yield addDelegate(txn, delegatorRecord, delegateRecord, readWrite)
+
+    yield txn.commit()
+
+
+
 class UpgradeFileSystemFormatStep(object):
     """
     Upgrade filesystem from previous versions.
     """
 
-    def __init__(self, config):
+    def __init__(self, config, store):
         """
         Initialize the service.
         """
         self.config = config
+        self.store = store
 
 
     @inlineCallbacks
@@ -985,7 +1068,7 @@
         memcacheEnabled = self.config.Memcached.Pools.Default.ClientEnabled
         self.config.Memcached.Pools.Default.ClientEnabled = False
 
-        yield upgradeData(self.config)
+        yield upgradeData(self.config, self.store.directoryService())
 
         # Restore memcached client setting
         self.config.Memcached.Pools.Default.ClientEnabled = memcacheEnabled
@@ -1010,6 +1093,8 @@
 
         1. Populating the group-membership cache
         2. Processing non-implicit inbox items
+        3. Migrate IMIP tokens into the store
+        4. Migrating delegate assignments into the store
     """
 
     def __init__(self, store, config, doPostImport):
@@ -1025,38 +1110,48 @@
     def stepWithResult(self, result):
         if self.doPostImport:
 
-            directory = directoryFromConfig(self.config)
-
             # Load proxy assignments from XML if specified
-            if self.config.ProxyLoadFromFile:
-                proxydbClass = namedClass(self.config.ProxyDBService.type)
-                calendaruserproxy.ProxyDBService = proxydbClass(
-                    **self.config.ProxyDBService.params)
-                loader = XMLCalendarUserProxyLoader(self.config.ProxyLoadFromFile)
-                yield loader.updateProxyDB()
+            if (
+                self.config.ProxyLoadFromFile and
+                os.path.exists(self.config.ProxyLoadFromFile) and
+                not os.path.exists(
+                    os.path.join(
+                        self.config.DataRoot, "proxies.sqlite"
+                    )
+                )
+            ):
+                sqliteProxyService = ProxySqliteDB("proxies.sqlite")
+                yield loadDelegatesFromXML(
+                    self.config.ProxyLoadFromFile,
+                    sqliteProxyService
+                )
+            else:
+                sqliteProxyService = None
 
-            # Populate the group membership cache
-            if (self.config.GroupCaching.Enabled and
-                self.config.GroupCaching.EnableUpdater):
-                proxydb = calendaruserproxy.ProxyDBService
-                if proxydb is None:
-                    proxydbClass = namedClass(self.config.ProxyDBService.type)
-                    proxydb = proxydbClass(**self.config.ProxyDBService.params)
 
-                updater = GroupMembershipCacheUpdater(proxydb,
-                    directory,
-                    self.config.GroupCaching.UpdateSeconds,
-                    self.config.GroupCaching.ExpireSeconds,
-                    self.config.GroupCaching.LockSeconds,
-                    namespace=self.config.GroupCaching.MemcachedPool,
-                    useExternalProxies=self.config.GroupCaching.UseExternalProxies)
-                yield updater.updateCache(fast=True)
+            # # Populate the group membership cache
+            # if (self.config.GroupCaching.Enabled and
+            #     self.config.GroupCaching.EnableUpdater):
+            #     proxydb = calendaruserproxy.ProxyDBService
+            #     if proxydb is None:
+            #         proxydbClass = namedClass(self.config.ProxyDBService.type)
+            #         proxydb = proxydbClass(**self.config.ProxyDBService.params)
 
-                uid, gid = getCalendarServerIDs(self.config)
-                dbPath = os.path.join(self.config.DataRoot, "proxies.sqlite")
-                if os.path.exists(dbPath):
-                    os.chown(dbPath, uid, gid)
+            #     # MOVE2WHO FIXME: port to new group cacher
+            #     updater = GroupMembershipCacheUpdater(proxydb,
+            #         directory,
+            #         self.config.GroupCaching.UpdateSeconds,
+            #         self.config.GroupCaching.ExpireSeconds,
+            #         self.config.GroupCaching.LockSeconds,
+            #         namespace=self.config.GroupCaching.MemcachedPool,
+            #         useExternalProxies=self.config.GroupCaching.UseExternalProxies)
+            #     yield updater.updateCache(fast=True)
 
+            uid, gid = getCalendarServerIDs(self.config)
+            dbPath = os.path.join(self.config.DataRoot, "proxies.sqlite")
+            if os.path.exists(dbPath):
+                os.chown(dbPath, uid, gid)
+
             # Process old inbox items
             self.store.setMigrating(True)
             yield self.processInboxItems()
@@ -1065,7 +1160,12 @@
             # Migrate mail tokens from sqlite to store
             yield migrateTokensToStore(self.config.DataRoot, self.store)
 
+            # Migrate delegate assignments from sqlite to store
+            if sqliteProxyService is None:
+                sqliteProxyService = ProxySqliteDB("proxies.sqlite")
+            yield migrateDelegatesToStore(sqliteProxyService, self.store)
 
+
     @inlineCallbacks
     def processInboxItems(self):
         """
@@ -1102,14 +1202,14 @@
                         inboxItems.remove(inboxItem)
                         continue
 
-                    record = directory.recordWithUID(uuid)
+                    record = yield directory.recordWithUID(uuid)
                     if record is None:
                         log.debug("Ignored inbox item - no record: %s" % (inboxItem,))
                         inboxItems.remove(inboxItem)
                         ignoreUUIDs.add(uuid)
                         continue
 
-                    principal = principalCollection.principalForRecord(record)
+                    principal = yield principalCollection.principalForRecord(record)
                     if principal is None or not isinstance(principal, DirectoryCalendarPrincipalResource):
                         log.debug("Ignored inbox item - no principal: %s" % (inboxItem,))
                         inboxItems.remove(inboxItem)
@@ -1117,7 +1217,7 @@
                         continue
 
                     request = FakeRequest(root, "PUT", None)
-                    request.noAttendeeRefresh = True # tell scheduling to skip refresh
+                    request.noAttendeeRefresh = True  # tell scheduling to skip refresh
                     request.checkedSACL = True
                     request.authnUser = request.authzUser = element.Principal(
                         element.HRef.fromString("/principals/__uids__/%s/" % (uuid,))
@@ -1156,8 +1256,10 @@
                                         uri
                                     )
                                 except Exception, e:
-                                    log.error("Error processing inbox item: %s (%s)"
-                                        % (inboxItem, e))
+                                    log.error(
+                                        "Error processing inbox item: %s (%s)"
+                                        % (inboxItem, e)
+                                    )
                             else:
                                 log.debug("Ignored inbox item - no resource: %s" % (inboxItem,))
                         else:
@@ -1194,8 +1296,10 @@
 
 
     @inlineCallbacks
-    def processInboxItem(self, root, directory, principal, request, inbox,
-        inboxItem, uuid, uri):
+    def processInboxItem(
+        self, root, directory, principal, request, inbox,
+        inboxItem, uuid, uri
+    ):
         """
         Run an individual inbox item through implicit scheduling and remove
         the inbox item.
@@ -1207,8 +1311,10 @@
 
         ownerPrincipal = principal
         cua = "urn:uuid:%s" % (uuid,)
-        owner = LocalCalendarUser(cua, ownerPrincipal,
-            inbox, ownerPrincipal.scheduleInboxURL())
+        owner = LocalCalendarUser(
+            cua, ownerPrincipal,
+            inbox, ownerPrincipal.scheduleInboxURL()
+        )
 
         calendar = yield inboxItem.iCalendar()
         if calendar.mainType() is not None:
@@ -1226,14 +1332,16 @@
                 originator = calendar.getOrganizer()
 
             principalCollection = directory.principalCollection
-            originatorPrincipal = principalCollection.principalForCalendarUserAddress(originator)
+            originatorPrincipal = yield principalCollection.principalForCalendarUserAddress(originator)
             originator = LocalCalendarUser(originator, originatorPrincipal)
             recipients = (owner,)
 
             scheduler = DirectScheduler(request, inboxItem)
             # Process inbox item
-            yield scheduler.doSchedulingViaPUT(originator, recipients, calendar,
-                internal_request=False, noAttendeeRefresh=True)
+            yield scheduler.doSchedulingViaPUT(
+                originator, recipients, calendar,
+                internal_request=False, noAttendeeRefresh=True
+            )
         else:
             log.warn("Removing invalid inbox item: %s" % (uri,))
 

Modified: CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/twistedcaldav/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -495,37 +495,7 @@
 
 
 
-def normalizationLookup(cuaddr, principalFunction, config):
-    """
-    Lookup function to be passed to ical.normalizeCalendarUserAddresses.
-    Returns a tuple of (Full name, guid, and calendar user address list)
-    for the given cuaddr.  The principalFunction is called to retrieve the
-    principal for the cuaddr.
-    """
-    try:
-        principal = principalFunction(cuaddr)
-    except Exception, e:
-        log.debug("Lookup of %s failed: %s" % (cuaddr, e))
-        principal = None
 
-    if principal is None:
-        return (None, None, None)
-    else:
-        rec = principal.record
-
-        # RFC5545 syntax does not allow backslash escaping in
-        # parameter values. A double-quote is thus not allowed
-        # in a parameter value except as the start/end delimiters.
-        # Single quotes are allowed, so we convert any double-quotes
-        # to single-quotes.
-        fullName = rec.fullName.replace('"', "'")
-
-        cuas = principal.record.calendarUserAddresses
-
-        return (fullName, rec.guid, cuas)
-
-
-
 def bestAcceptType(accepts, allowedTypes):
     """
     Given a set of Accept headers and the set of types the server can return, determine the best choice

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/caldav/scheduler.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/caldav/scheduler.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/caldav/scheduler.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -85,13 +85,14 @@
             ))
 
 
+    @inlineCallbacks
     def checkOriginator(self):
         """
         Check the validity of the Originator header. Extract the corresponding principal.
         """
 
         # Verify that Originator is a valid calendar user
-        originatorPrincipal = self.txn.directoryService().recordWithCalendarUserAddress(self.originator)
+        originatorPrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(self.originator)
         if originatorPrincipal is None:
             # Local requests MUST have a principal.
             log.error("Could not find principal for originator: %s" % (self.originator,))
@@ -122,7 +123,7 @@
         results = []
         for recipient in self.recipients:
             # Get the principal resource for this recipient
-            principal = self.txn.directoryService().recordWithCalendarUserAddress(recipient)
+            principal = yield self.txn.directoryService().recordWithCalendarUserAddress(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
@@ -161,7 +162,7 @@
         # Verify that the ORGANIZER's cu address maps to a valid user
         organizer = self.calendar.getOrganizer()
         if organizer:
-            organizerPrincipal = self.txn.directoryService().recordWithCalendarUserAddress(organizer)
+            organizerPrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(organizer)
             if organizerPrincipal:
                 if organizerPrincipal.calendarsEnabled():
 
@@ -225,6 +226,7 @@
             ))
 
 
+    @inlineCallbacks
     def checkAttendeeAsOriginator(self):
         """
         Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
@@ -232,7 +234,7 @@
         """
 
         # Attendee's Outbox MUST be the request URI
-        attendeePrincipal = self.txn.directoryService().recordWithCalendarUserAddress(self.attendee)
+        attendeePrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(self.attendee)
         if attendeePrincipal:
             if self.doingPOST is not None and attendeePrincipal.uid != self.originator_uid:
                 log.error("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
@@ -257,11 +259,11 @@
 
         # Prevent spoofing of ORGANIZER with specific METHODs when local
         if self.isiTIPRequest:
-            self.checkOrganizerAsOriginator()
+            return self.checkOrganizerAsOriginator()
 
         # Prevent spoofing when doing reply-like METHODs
         else:
-            self.checkAttendeeAsOriginator()
+            return self.checkAttendeeAsOriginator()
 
 
     def finalChecks(self):

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/freebusy.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/freebusy.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -47,6 +47,8 @@
 
 fbcacher = Memcacher("FBCache", pickle=True)
 
+
+
 class FBCacheEntry(object):
 
     CACHE_DAYS_FLOATING_ADJUST = 1
@@ -212,12 +214,12 @@
     # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy
 
     # May need organizer principal
-    organizer_record = calresource.directoryService().recordWithCalendarUserAddress(organizer) if organizer else None
+    organizer_record = (yield calresource.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None
     organizer_uid = organizer_record.uid if organizer_record else ""
 
     # Free busy is per-user
     attendee_uid = calresource.viewerHome().uid()
-    attendee_record = calresource.directoryService().recordWithUID(attendee_uid)
+    attendee_record = yield calresource.directoryService().recordWithUID(attendee_uid.decode("utf-8"))
 
     # Get the timezone property from the collection.
     tz = calresource.getTimezone()
@@ -237,7 +239,7 @@
         authz_record = organizer_record
         if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
             authz_uid = calresource._txn._authz_uid
-            authz_record = calresource.directoryService().recordWithUID(authz_uid)
+            authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8"))
 
         # Check if attendee is also the organizer or the delegate doing the request
         if attendee_uid in (organizer_uid, authz_uid):
@@ -335,7 +337,7 @@
                 if excludeuid:
                     # See if we have a UID match
                     if (excludeuid == uid):
-                        test_record = calresource.directoryService().recordWithCalendarUserAddress(test_organizer) if test_organizer else None
+                        test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer)) if test_organizer else None
                         test_uid = test_record.uid if test_record else ""
 
                         # Check that ORGANIZER's match (security requirement)
@@ -395,7 +397,7 @@
                 # See if we have a UID match
                 if (excludeuid == uid):
                     test_organizer = calendar.getOrganizer()
-                    test_record = calresource.principalForCalendarUserAddress(test_organizer) if test_organizer else None
+                    test_record = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
                     test_uid = test_record.principalUID() if test_record else ""
 
                     # Check that ORGANIZER's match (security requirement)

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/imip/inbound.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/imip/inbound.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/imip/inbound.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -329,9 +329,12 @@
                 toAddr = organizer[7:]
             elif organizer.startswith("urn:uuid:"):
                 guid = organizer[9:]
-                record = self.directory.recordWithGUID(guid)
-                if record and record.emailAddresses:
-                    toAddr = list(record.emailAddresses)[0]
+                record = yield self.directory.recordWithGUID(guid)
+                try:
+                    if record and record.emailAddresses:
+                        toAddr = list(record.emailAddresses)[0]
+                except AttributeError:
+                    pass
 
             if toAddr is None:
                 log.error("Don't have an email address for the organizer; "

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/implicit.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/implicit.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -297,7 +297,7 @@
         organizer_scheduling = (yield self.isOrganizerScheduling())
         if organizer_scheduling:
             self.state = "organizer"
-        elif self.isAttendeeScheduling():
+        elif (yield self.isAttendeeScheduling()):
             self.state = "attendee"
         elif self.organizer:
             # There is an ORGANIZER that is not this user but no ATTENDEE property for
@@ -365,7 +365,7 @@
 
         # Get some useful information from the calendar
         yield self.extractCalendarData()
-        self.organizerPrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
+        self.organizerPrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
         self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
 
         # Originator is the organizer in this case
@@ -447,7 +447,7 @@
             self.calendar = calendar_old
 
         yield self.extractCalendarData()
-        self.organizerPrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
+        self.organizerPrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
         self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
 
         # Originator is the organizer in this case
@@ -479,7 +479,7 @@
         # Get some useful information from the calendar
         yield self.extractCalendarData()
 
-        self.attendeePrincipal = self.calendar_home.directoryService().recordWithUID(self.calendar_home.uid())
+        self.attendeePrincipal = yield self.calendar_home.directoryService().recordWithUID(self.calendar_home.uid().decode("utf-8"))
         self.originator = self.attendee = self.attendeePrincipal.canonicalCalendarUserAddress()
 
         result = (yield self.scheduleWithOrganizer())
@@ -491,7 +491,7 @@
     def extractCalendarData(self):
 
         # Get the originator who is the owner of the calendar resource being modified
-        self.originatorPrincipal = self.calendar_home.directoryService().recordWithUID(self.calendar_home.uid())
+        self.originatorPrincipal = yield self.calendar_home.directoryService().recordWithUID(self.calendar_home.uid().decode("utf-8"))
 
         # Pick the canonical CUA:
         self.originator = self.originatorPrincipal.canonicalCalendarUserAddress()
@@ -555,7 +555,7 @@
             returnValue(False)
 
         # Organizer must map to a valid principal
-        self.organizerPrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
+        self.organizerPrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(self.organizer)
         self.organizerAddress = (yield addressmapping.mapper.getCalendarUser(self.organizer, self.organizerPrincipal))
         if not self.organizerPrincipal:
             returnValue(False)
@@ -567,21 +567,22 @@
         returnValue(True)
 
 
+    @inlineCallbacks
     def isAttendeeScheduling(self):
 
         # First must have organizer property
         if not self.organizer:
-            return False
+            returnValue(False)
 
         # Check to see whether any attendee is the owner
         for attendee in self.attendees:
-            attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+            attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
             if attendeePrincipal and attendeePrincipal.uid == self.calendar_home.uid():
                 self.attendee = attendee
                 self.attendeePrincipal = attendeePrincipal
-                return True
+                returnValue(True)
 
-        return False
+        returnValue(False)
 
 
     def makeScheduler(self):
@@ -632,7 +633,7 @@
                 self.calendar.sequenceInSync(self.oldcalendar)
 
             # Significant change
-            no_change, self.changed_rids, self.needs_action_rids, reinvites, recurrence_reschedule, status_cancelled, only_status = self.isOrganizerChangeInsignificant()
+            no_change, self.changed_rids, self.needs_action_rids, reinvites, recurrence_reschedule, status_cancelled, only_status = yield self.isOrganizerChangeInsignificant()
             if no_change:
                 if reinvites:
                     log.debug("Implicit - organizer '{organizer}' is re-inviting UID: '{uid}', attendees: {attendees}", organizer=self.organizer, uid=self.uid, attendees=", ".join(reinvites))
@@ -720,6 +721,7 @@
                 pass
 
 
+    @inlineCallbacks
     def isOrganizerChangeInsignificant(self):
 
         rids = None
@@ -802,15 +804,16 @@
                     only_status = False
 
             if checkOrganizerValue:
+                @inlineCallbacks
                 def _normalizeCUAddress(addr):
                     if not addr.startswith("urn:uuid"):
-                        principal = self.calendar_home.directoryService().recordWithCalendarUserAddress(addr)
+                        principal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(addr)
                         if principal is not None:
                             addr = principal.canonicalCalendarUserAddress()
-                    return addr
+                    returnValue(addr)
 
-                oldOrganizer = _normalizeCUAddress(self.oldcalendar.getOrganizer())
-                newOrganizer = _normalizeCUAddress(self.calendar.getOrganizer())
+                oldOrganizer = yield _normalizeCUAddress(self.oldcalendar.getOrganizer())
+                newOrganizer = yield _normalizeCUAddress(self.calendar.getOrganizer())
                 if oldOrganizer != newOrganizer:
                     log.error("Cannot change ORGANIZER: UID:{uid}", uid=self.uid)
                     raise HTTPError(ErrorResponse(
@@ -828,7 +831,10 @@
                 except KeyError:
                     pass
 
-        return no_change, rids, date_changed_rids, reinvites, recurrence_reschedule, status_cancelled, only_status
+        returnValue((
+            no_change, rids, date_changed_rids, reinvites,
+            recurrence_reschedule, status_cancelled, only_status
+        ))
 
 
     def findRemovedAttendees(self):
@@ -1043,7 +1049,7 @@
             if attendee.parameterValue("SCHEDULE-AGENT", "SERVER").upper() == "CLIENT":
                 cuaddr = attendee.value()
                 if cuaddr not in coerced:
-                    attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(cuaddr)
+                    attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(cuaddr)
                     attendeeAddress = (yield addressmapping.mapper.getCalendarUser(cuaddr, attendeePrincipal))
                     local_attendee = type(attendeeAddress) in (LocalCalendarUser, OtherServerCalendarUser,)
                     coerced[cuaddr] = local_attendee
@@ -1116,7 +1122,7 @@
 
             # Handle split by not scheduling local attendees
             if self.split_details is not None:
-                attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
                 attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
                 if type(attendeeAddress) is LocalCalendarUser:
                     continue
@@ -1173,7 +1179,7 @@
 
             # Handle split by not scheduling local attendees
             if self.split_details is not None:
-                attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
                 attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
                 if type(attendeeAddress) is LocalCalendarUser:
                     continue
@@ -1233,7 +1239,7 @@
 
             # Handle split by not scheduling local attendees
             if self.split_details is not None:
-                attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
                 attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
                 if type(attendeeAddress) is LocalCalendarUser:
                     continue
@@ -1298,7 +1304,7 @@
 
             # Handle split by not scheduling local attendees
             if self.split_details is not None:
-                attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
                 attendeeAddress = (yield addressmapping.mapper.getCalendarUser(attendee, attendeePrincipal))
                 if type(attendeeAddress) is LocalCalendarUser:
                     continue
@@ -1516,7 +1522,7 @@
                     oldattendess = self.oldcalendar.getAllUniqueAttendees()
                     found_old = False
                     for attendee in oldattendess:
-                        attendeePrincipal = self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
+                        attendeePrincipal = yield self.calendar_home.directoryService().recordWithCalendarUserAddress(attendee)
                         if attendeePrincipal and attendeePrincipal.uid == self.calendar_home.uid():
                             found_old = True
                             break

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/delivery.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/delivery.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/delivery.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -228,7 +228,7 @@
             # Loop over at most 3 redirects
             ssl, host, port, path = self.server.details()
             for _ignore in xrange(3):
-                self._prepareRequest(host, port)
+                yield self._prepareRequest(host, port)
                 response = (yield self._processRequest(ssl, host, port, path))
                 if response.code not in (responsecode.MOVED_PERMANENTLY, responsecode.TEMPORARY_REDIRECT,):
                     break
@@ -334,16 +334,18 @@
         returnValue(iostr.getvalue())
 
 
+    @inlineCallbacks
     def _prepareRequest(self, host, port):
         """
         Setup the request for sending. We might need to do this several times
         whilst following redirects.
         """
 
-        component, method = self._prepareData()
-        self._prepareHeaders(host, port, component, method)
+        component, method = (yield self._prepareData())
+        yield self._prepareHeaders(host, port, component, method)
 
 
+    @inlineCallbacks
     def _prepareHeaders(self, host, port, component, method):
         """
         Always generate a new set of headers because the Host may varying during redirects,
@@ -357,7 +359,7 @@
         # The Originator must be the ORGANIZER (for a request) or ATTENDEE (for a reply)
         originator = self.scheduler.organizer.cuaddr if self.scheduler.isiTIPRequest else self.scheduler.attendee
         if self.server.unNormalizeAddresses:
-            originator = normalizeCUAddress(originator, normalizationLookup, self.scheduler.txn.directoryService().recordWithCalendarUserAddress, toUUID=False)
+            originator = yield normalizeCUAddress(originator, normalizationLookup, self.scheduler.txn.directoryService().recordWithCalendarUserAddress, toUUID=False)
         self.headers.addRawHeader("Originator", utf8String(originator))
         self.sign_headers.append("Originator")
 
@@ -399,6 +401,7 @@
             self.sign_headers.append("Authorization")
 
 
+    @inlineCallbacks
     def _prepareData(self):
         """
         Prepare data via normalization etc. Only need to do this once even when
@@ -411,7 +414,7 @@
             normalizedCalendar = self.scheduler.calendar.duplicate()
             self.original_organizer = normalizedCalendar.getOrganizer()
             if self.server.unNormalizeAddresses:
-                normalizedCalendar.normalizeCalendarUserAddresses(
+                yield normalizedCalendar.normalizeCalendarUserAddresses(
                     normalizationLookup,
                     self.scheduler.txn.directoryService().recordWithCalendarUserAddress,
                     toUUID=False)
@@ -423,12 +426,12 @@
             component = normalizedCalendar.mainType()
             method = normalizedCalendar.propertyValue("METHOD")
             self.data = str(normalizedCalendar)
-            return component, method
+            returnValue(component, method)
         else:
             cal = Component.fromString(self.data)
             component = cal.mainType()
             method = cal.propertyValue("METHOD")
-            return component, method
+            returnValue(component, method)
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -99,12 +99,13 @@
         return False
 
 
+    @inlineCallbacks
     def principalForCalendarUserAddress(self, address):
         for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
+            principal = yield principalCollection.principalForCalendarUserAddress(address)
             if principal is not None:
-                return principal
-        return None
+                returnValue(principal)
+        returnValue(None)
 
 
     def render(self, request):
@@ -353,11 +354,13 @@
             davxml.Privilege(caldavxml.ScheduleDeliver()),
         )
 
-        return davxml.ACL(
-            # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
+        return succeed(
+            davxml.ACL(
+                # DAV:Read, CalDAV:schedule-deliver for all principals (includes anonymous)
+                davxml.ACE(
+                    davxml.Principal(davxml.All()),
+                    davxml.Grant(*privs),
+                    davxml.Protected(),
+                ),
+            )
         )

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/scheduler.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/ischedule/scheduler.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -186,7 +186,7 @@
         # Normalize recipient addresses
         results = []
         for recipient in recipients:
-            normalized = normalizeCUAddress(recipient, normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress)
+            normalized = yield normalizeCUAddress(recipient, normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress)
             self.recipientsNormalizationMap[normalized] = recipient
             results.append(normalized)
         recipients = results
@@ -205,7 +205,7 @@
         if not self.checkForFreeBusy():
             # Need to normalize the calendar data and recipient values to keep those in sync,
             # as we might later try to match them
-            self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress)
+            return self.calendar.normalizeCalendarUserAddresses(normalizationLookup, self.txn.directoryService().recordWithCalendarUserAddress)
 
 
     def checkAuthorization(self):
@@ -226,7 +226,7 @@
         """
 
         # For remote requests we do not allow the originator to be a local user or one within our domain.
-        originatorPrincipal = self.txn.directoryService().recordWithCalendarUserAddress(self.originator)
+        originatorPrincipal = (yield self.txn.directoryService().recordWithCalendarUserAddress(self.originator))
         localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
         if originatorPrincipal or localUser:
             if originatorPrincipal.thisServer():
@@ -367,7 +367,7 @@
         # Verify that the ORGANIZER's cu address does not map to a valid user
         organizer = self.calendar.getOrganizer()
         if organizer:
-            organizerPrincipal = self.txn.directoryService().recordWithCalendarUserAddress(organizer)
+            organizerPrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(organizer)
             if organizerPrincipal:
                 if organizerPrincipal.thisServer():
                     log.error("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
@@ -408,7 +408,7 @@
         """
 
         # Attendee cannot be local.
-        attendeePrincipal = self.txn.directoryService().recordWithCalendarUserAddress(self.attendee)
+        attendeePrincipal = yield self.txn.directoryService().recordWithCalendarUserAddress(self.attendee)
         if attendeePrincipal:
             if attendeePrincipal.thisServer():
                 log.error("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/itip.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/itip.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -42,6 +42,7 @@
     "iTipGenerator",
 ]
 
+
 class iTipProcessing(object):
 
     @staticmethod

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/processing.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/processing.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -36,6 +36,7 @@
 from txdav.caldav.datastore.scheduling.work import ScheduleRefreshWork, \
     ScheduleAutoReplyWork
 from txdav.caldav.icalendarstore import ComponentUpdateState, ComponentRemoveState
+from txdav.who.idirectory import AutoScheduleMode
 
 import collections
 import hashlib
@@ -58,6 +59,8 @@
 
 log = Logger()
 
+
+
 class ImplicitProcessorException(Exception):
 
     def __init__(self, msg):
@@ -431,9 +434,9 @@
 
             # Handle auto-reply behavior
             organizer = normalizeCUAddr(self.message.getOrganizer())
-            if self.recipient.principal.canAutoSchedule(organizer=organizer):
+            if (yield self.recipient.principal.canAutoSchedule(organizer=organizer)):
                 # auto schedule mode can depend on who the organizer is
-                mode = self.recipient.principal.getAutoScheduleMode(organizer=organizer)
+                mode = yield self.recipient.principal.getAutoScheduleMode(organizer=organizer)
                 send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, mode))
 
                 # Only store inbox item when reply is not sent or always for users
@@ -464,9 +467,9 @@
 
                 # Handle auto-reply behavior
                 organizer = normalizeCUAddr(self.message.getOrganizer())
-                if self.recipient.principal.canAutoSchedule(organizer=organizer) and not hasattr(self.txn, "doing_attendee_refresh"):
+                if (yield self.recipient.principal.canAutoSchedule(organizer=organizer)) and not hasattr(self.txn, "doing_attendee_refresh"):
                     # auto schedule mode can depend on who the organizer is
-                    mode = self.recipient.principal.getAutoScheduleMode(organizer=organizer)
+                    mode = yield self.recipient.principal.getAutoScheduleMode(organizer=organizer)
                     send_reply, store_inbox, partstat = (yield self.checkAttendeeAutoReply(new_calendar, mode))
 
                     # Only store inbox item when reply is not sent or always for users
@@ -545,7 +548,7 @@
             # inbox item on them even if auto-schedule is true so that they get a notification
             # of the cancel.
             organizer = normalizeCUAddr(self.message.getOrganizer())
-            autoprocessed = self.recipient.principal.canAutoSchedule(organizer=organizer)
+            autoprocessed = yield self.recipient.principal.canAutoSchedule(organizer=organizer)
             store_inbox = not autoprocessed or self.recipient.principal.getCUType() == "INDIVIDUAL"
 
             # Check to see if this is a cancel of the entire event
@@ -604,19 +607,28 @@
         @param calendar: the iTIP message to process
         @type calendar: L{Component}
         @param automode: the auto-schedule mode for the recipient
-        @type automode: C{str}
+        @type automode: L{txdav.who.idirectory.AutoScheduleMode}
 
         @return: C{tuple} of C{bool}, C{bool}, C{str} indicating whether changes were made, whether the inbox item
             should be added, and the new PARTSTAT.
         """
-
         # First ignore the none mode
-        if automode == "none":
+        if automode == AutoScheduleMode.none:
             returnValue((False, True, "",))
-        elif not automode or automode == "default":
-            automode = config.Scheduling.Options.AutoSchedule.DefaultMode
+        elif not automode:
+            automode = {
+                "none": AutoScheduleMode.none,
+                "accept-always": AutoScheduleMode.accept,
+                "decline-always": AutoScheduleMode.decline,
+                "accept-if-free": AutoScheduleMode.acceptIfFree,
+                "decline-if-busy": AutoScheduleMode.declineIfBusy,
+                "automatic": AutoScheduleMode.acceptIfFreeDeclineIfBusy,
+            }.get(
+                config.Scheduling.Options.AutoSchedule.DefaultMode,
+                AutoScheduleMode.acceptIfFreeDeclineIfBusy
+            )
 
-        log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode,))
+        log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply with mode: %s" % (self.recipient.cuaddr, self.uid, automode.name,))
 
         cuas = self.recipient.principal.calendarUserAddresses
 
@@ -704,13 +716,19 @@
         partstat_counts = collections.defaultdict(int)
         for instance in instances.instances.itervalues():
             if instance.partstat == "NEEDS-ACTION" and instance.active:
-                if automode == "accept-always":
+                if automode == AutoScheduleMode.accept:
                     freePartstat = busyPartstat = "ACCEPTED"
-                elif automode == "decline-always":
+                elif automode == AutoScheduleMode.decline:
                     freePartstat = busyPartstat = "DECLINED"
                 else:
-                    freePartstat = "ACCEPTED" if automode in ("accept-if-free", "automatic",) else "NEEDS-ACTION"
-                    busyPartstat = "DECLINED" if automode in ("decline-if-busy", "automatic",) else "NEEDS-ACTION"
+                    freePartstat = "ACCEPTED" if automode in (
+                        AutoScheduleMode.acceptIfFree,
+                        AutoScheduleMode.acceptIfFreeDeclineIfBusy,
+                    ) else "NEEDS-ACTION"
+                    busyPartstat = "DECLINED" if automode in (
+                        AutoScheduleMode.declineIfBusy,
+                        AutoScheduleMode.acceptIfFreeDeclineIfBusy,
+                    ) else "NEEDS-ACTION"
                 instance.partstat = freePartstat if instance.free else busyPartstat
             partstat_counts[instance.partstat] += 1
 
@@ -901,7 +919,7 @@
 
         # We only need to fix data that already exists
         if recipient_resource is not None:
-            if originator_calendar.mainType() != None:
+            if originator_calendar.mainType() is not None:
                 yield self.writeCalendarResource(None, recipient_resource, originator_calendar)
             else:
                 yield self.deleteCalendarResource(recipient_resource)

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/scheduler.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/scheduler.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -162,7 +162,7 @@
         """
 
         self.calendar = calendar
-        self.preProcessCalendarData()
+        yield self.preProcessCalendarData()
 
         if self.logItems is not None:
             self.logItems["recipients"] = len(recipients)
@@ -550,7 +550,7 @@
         results = []
         for recipient in self.recipients:
             # Get the principal resource for this recipient
-            principal = self.txn.directoryService().recordWithCalendarUserAddress(recipient)
+            principal = yield self.txn.directoryService().recordWithCalendarUserAddress(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

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/work.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/work.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/scheduling/work.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -45,6 +45,8 @@
 
 log = Logger()
 
+
+
 class ScheduleWorkMixin(object):
     """
     Base class for common schedule work item behavior.
@@ -189,7 +191,7 @@
         try:
             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
             resource = (yield home.objectResourceWithID(self.resourceID))
-            organizerPrincipal = home.directoryService().recordWithUID(home.uid())
+            organizerPrincipal = yield home.directoryService().recordWithUID(home.uid().decode("utf-8"))
             organizer = organizerPrincipal.canonicalCalendarUserAddress()
             calendar_old = Component.fromString(self.icalendarTextOld) if self.icalendarTextOld else None
             calendar_new = Component.fromString(self.icalendarTextNew) if self.icalendarTextNew else None
@@ -311,7 +313,7 @@
         try:
             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
             resource = (yield home.objectResourceWithID(self.resourceID))
-            attendeePrincipal = home.directoryService().recordWithUID(home.uid())
+            attendeePrincipal = yield home.directoryService().recordWithUID(home.uid().decode("utf-8"))
             attendee = attendeePrincipal.canonicalCalendarUserAddress()
             calendar = (yield resource.componentForUser())
             organizer = calendar.validOrganizerForScheduling()
@@ -336,6 +338,7 @@
             self._dequeued()
 
         except Exception, e:
+            # FIXME: calendar may not be set here!
             log.debug("ScheduleReplyWork - exception ID: {id}, UID: '{uid}', {err}", id=self.workID, uid=calendar.resourceUID(), err=str(e))
             raise
         except:
@@ -381,7 +384,7 @@
 
         try:
             home = (yield self.transaction.calendarHomeWithResourceID(self.homeResourceID))
-            attendeePrincipal = home.directoryService().recordWithUID(home.uid())
+            attendeePrincipal = yield home.directoryService().recordWithUID(home.uid().decode("utf-8"))
             attendee = attendeePrincipal.canonicalCalendarUserAddress()
             calendar = Component.fromString(self.icalendarText)
             organizer = calendar.validOrganizerForScheduling()

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/sql.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/sql.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1915,14 +1915,14 @@
 
             # Normalize the calendar user addresses once we know we have valid
             # calendar data
-            component.normalizeCalendarUserAddresses(normalizationLookup, self.directoryService().recordWithCalendarUserAddress)
+            yield component.normalizeCalendarUserAddresses(normalizationLookup, self.directoryService().recordWithCalendarUserAddress)
 
         # Possible timezone stripping
         if config.EnableTimezonesByReference:
             component.stripKnownTimezones()
 
         # Check location/resource organizer requirement
-        self.validLocationResourceOrganizer(component, inserting, internal_state)
+        yield self.validLocationResourceOrganizer(component, inserting, internal_state)
 
         # Check access
         if config.EnablePrivateEvents:
@@ -1986,13 +1986,14 @@
                     raise TooManyAttendeesError("Attendee list size %d is larger than allowed limit %d" % (attendeeListLength, config.MaxAttendeesPerInstance))
 
 
+    @inlineCallbacks
     def validLocationResourceOrganizer(self, component, inserting, internal_state):
         """
         If the calendar owner is a location or resource, check whether an ORGANIZER property is required.
         """
 
         if internal_state == ComponentUpdateState.NORMAL:
-            originatorPrincipal = self.calendar().ownerHome().directoryRecord()
+            originatorPrincipal = yield self.calendar().ownerHome().directoryRecord()
             cutype = originatorPrincipal.getCUType() if originatorPrincipal is not None else "INDIVIDUAL"
             organizer = component.getOrganizer()
 
@@ -2009,9 +2010,9 @@
 
                 # Find current principal and update modified by details
                 if self._txn._authz_uid is not None:
-                    authz = self.directoryService().recordWithUID(self._txn._authz_uid)
+                    authz = yield self.directoryService().recordWithUID(self._txn._authz_uid.decode("utf-8"))
                     prop = Property("X-CALENDARSERVER-MODIFIED-BY", authz.canonicalCalendarUserAddress())
-                    prop.setParameter("CN", authz.displayName())
+                    prop.setParameter("CN", authz.displayName)
                     for candidate in authz.calendarUserAddresses:
                         if candidate.startswith("mailto:"):
                             prop.setParameter("EMAIL", candidate[7:])
@@ -2108,7 +2109,7 @@
                 log.debug("Organizer and attendee properties were entirely removed by the client. Restoring existing properties.")
 
                 # Get the originator who is the owner of the calendar resource being modified
-                originatorPrincipal = self.calendar().ownerHome().directoryRecord()
+                originatorPrincipal = yield self.calendar().ownerHome().directoryRecord()
                 originatorAddresses = originatorPrincipal.calendarUserAddresses
 
                 for component in calendar.subcomponents():
@@ -2145,7 +2146,7 @@
                 log.debug("Sync COMPLETED property change.")
 
                 # Get the originator who is the owner of the calendar resource being modified
-                originatorPrincipal = self.calendar().ownerHome().directoryRecord()
+                originatorPrincipal = yield self.calendar().ownerHome().directoryRecord()
                 originatorAddresses = originatorPrincipal.calendarUserAddresses
 
                 for component in calendar.subcomponents():
@@ -2264,6 +2265,7 @@
             self._componentChanged = True
 
 
+    @inlineCallbacks
     def addStructuredLocation(self, component):
         """
         Scan the component for ROOM attendees; if any are associated with an
@@ -2271,34 +2273,33 @@
         X-APPLE-STRUCTURED-LOCATION property and update the LOCATION property
         to contain the name and street address.
         """
+        dir = self.directoryService()
         for sub in component.subcomponents():
             for attendee in sub.getAllAttendeeProperties():
                 if attendee.parameterValue("CUTYPE") == "ROOM":
                     value = attendee.value()
-                    if value.startswith("urn:uuid:"):
-                        guid = value[9:]
-                        loc = self.directoryService().recordWithGUID(guid)
-                        if loc is not None:
-                            guid = loc.extras.get("associatedAddress",
-                                None)
-                            if guid is not None:
-                                addr = self.directoryService().recordWithGUID(guid)
-                                if addr is not None:
-                                    street = addr.extras.get("streetAddress", "")
-                                    geo = addr.extras.get("geo", "")
-                                    if street and geo:
-                                        title = attendee.parameterValue("CN")
-                                        params = {
-                                            "X-ADDRESS" : street,
-                                            "X-APPLE-RADIUS" : "71",
-                                            "X-TITLE" : title,
-                                        }
-                                        structured = Property("X-APPLE-STRUCTURED-LOCATION",
-                                            "geo:%s" % (geo,), params=params,
-                                            valuetype=Value.VALUETYPE_URI)
-                                        sub.replaceProperty(structured)
-                                        sub.replaceProperty(Property("LOCATION",
-                                            "%s\n%s" % (title, street)))
+                    loc = yield dir.recordWithCalendarUserAddress(value)
+                    if loc is not None:
+                        uid = getattr(loc, "associatedAddress", "")
+                        if uid:
+                            addr = yield dir.recordWithUID(uid)
+                            if addr is not None:
+                                street = getattr(addr, "streetAddress", "")
+                                geo = getattr(addr, "geographicLocation", "")
+                                if street and geo:
+                                    title = attendee.parameterValue("CN")
+                                    params = {
+                                        "X-ADDRESS": street,
+                                        "X-APPLE-RADIUS": "71",
+                                        "X-TITLE": title,
+                                    }
+                                    structured = Property("X-APPLE-STRUCTURED-LOCATION",
+                                        "geo:%s" % (geo.encode("utf-8"),), params=params,
+                                        valuetype=Value.VALUETYPE_URI)
+                                    sub.replaceProperty(structured)
+                                    newLocProperty = Property("LOCATION",
+                                        "%s\n%s" % (title, street.encode("utf-8")))
+                                    sub.replaceProperty(newLocProperty)
 
 
     @inlineCallbacks
@@ -2539,7 +2540,7 @@
             self.processAlarms(component, inserting)
 
             # Process structured location
-            self.addStructuredLocation(component)
+            yield self.addStructuredLocation(component)
 
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling(component, inserting, internal_state))
@@ -3760,9 +3761,9 @@
             raise InvalidSplit()
 
         # Cannot be attendee
-        ownerPrincipal = self.calendar().ownerHome().directoryRecord()
+        ownerPrincipal = yield self.calendar().ownerHome().directoryRecord()
         organizer = component.getOrganizer()
-        organizerPrincipal = self.directoryService().recordWithCalendarUserAddress(organizer) if organizer else None
+        organizerPrincipal = (yield self.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None
         if organizer is not None and organizerPrincipal.uid != ownerPrincipal.uid:
             raise InvalidSplit()
 

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/accounts.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/accounts.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/accounts.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,19 +18,19 @@
 
 <!DOCTYPE accounts SYSTEM "../../../conf/auth/accounts.dtd">
 
-<accounts realm="/Search">
-  <user>
+<directory realm="/Search">
+  <record type="user">
     <uid>home1</uid>
-    <guid>home1</guid>
+    <short-name>home1</short-name>
     <password>home1</password>
-    <name>Example User 1</name>
-    <email-address>home1 at example.com</email-address>
-  </user>
-  <user>
+    <full-name>Example User 1</full-name>
+    <email>home1 at example.com</email>
+  </record>
+  <record type="user">
     <uid>home2</uid>
-    <guid>home2</guid>
+    <short-name>home2</short-name>
     <password>home2</password>
-    <name>Example User 2</name>
-    <email-address>home2 at example.com</email-address>
-  </user>
-</accounts>
+    <full-name>Example User 2</full-name>
+    <email>home2 at example.com</email>
+  </record>
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/resources.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/resources.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/attachments/resources.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 
-<accounts realm="/Search">
-</accounts>
+<directory realm="/Search">
+</directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/test_attachments.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/test_attachments.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/test_attachments.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -14,7 +14,7 @@
 # limitations under the License.
 ##
 
-from calendarserver.tap.util import directoryFromConfig
+from txdav.who.util import directoryFromConfig
 
 from pycalendar.datetime import DateTime
 from pycalendar.value import Value
@@ -49,7 +49,7 @@
 """
 
 storePath = FilePath(__file__).parent().child("calendar_store")
-homeRoot = storePath.child("ho").child("me").child("home1")
+homeRoot = storePath.child("ho").child("me").child(u"home1")
 cal1Root = homeRoot.child("calendar_1")
 
 calendar1_objectNames = [
@@ -597,7 +597,7 @@
 
         self.assertTrue(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -609,7 +609,7 @@
 
         self.assertFalse(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertEqual(quota, 0)
@@ -648,7 +648,7 @@
         self.assertTrue(os.path.exists(apath1))
         self.assertTrue(os.path.exists(apath2))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -661,7 +661,7 @@
         self.assertFalse(os.path.exists(apath1))
         self.assertFalse(os.path.exists(apath2))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertEqual(quota, 0)
@@ -743,7 +743,7 @@
 
         self.assertTrue(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -755,7 +755,7 @@
 
         self.assertTrue(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -767,7 +767,7 @@
 
         self.assertFalse(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertEqual(quota, 0)
@@ -1131,7 +1131,7 @@
 
         self.assertTrue(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -1143,7 +1143,7 @@
 
         self.assertFalse(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertEqual(quota, 0)
@@ -1178,7 +1178,7 @@
         self.assertTrue(os.path.exists(apath1))
         self.assertTrue(os.path.exists(apath2))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -1191,7 +1191,7 @@
         self.assertFalse(os.path.exists(apath1))
         self.assertFalse(os.path.exists(apath2))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertEqual(quota, 0)
@@ -1218,7 +1218,7 @@
 
         self.assertTrue(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -1230,7 +1230,7 @@
 
         self.assertTrue(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertNotEqual(quota, 0)
@@ -1242,7 +1242,7 @@
 
         self.assertFalse(os.path.exists(apath))
 
-        home = (yield self.transactionUnderTest().calendarHomeWithUID("home1"))
+        home = (yield self.transactionUnderTest().calendarHomeWithUID(u"home1"))
         quota = (yield home.quotaUsedBytes())
         yield self.commit()
         self.assertEqual(quota, 0)
@@ -1363,7 +1363,7 @@
     }
 
     requirements = {
-        "home1" : {
+        u"home1" : {
             "calendar1" : {
                 "1.1.ics" : (PLAIN_ICS % {"year": now, "uid": "1.1", }, metadata,),
                 "1.2.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.2", "userid": "user01", "dropboxid": "1.2"}, metadata,),
@@ -1372,7 +1372,7 @@
                 "1.5.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "1.5", "userid": "user01", "dropboxid": "1.4"}, metadata,),
             }
         },
-        "home2" : {
+        u"home2" : {
             "calendar2" : {
                 "2-2.1.ics" : (PLAIN_ICS % {"year": now, "uid": "2-2.1", }, metadata,),
                 "2-2.2.ics" : (ATTACHMENT_ICS % {"year": now, "uid": "2-2.2", "userid": "user02", "dropboxid": "2.2"}, metadata,),
@@ -1488,16 +1488,16 @@
         """
         Add the full set of attachments to be used for testing.
         """
-        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_1.txt")
-        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_2.txt")
-        yield self._addAttachment("home1", "calendar1", "1.3.ics", "1.3", "attach_1_3.txt")
-        yield self._addAttachment("home1", "calendar1", "1.4.ics", "1.4", "attach_1_4.txt")
-        yield self._addAttachmentProperty("home1", "calendar1", "1.5.ics", "1.4", "home1", "attach_1_4.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_1.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_2.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.3.ics", "1.3", "attach_1_3.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.4.ics", "1.4", "attach_1_4.txt")
+        yield self._addAttachmentProperty(u"home1", "calendar1", "1.5.ics", "1.4", "home1", "attach_1_4.txt")
 
-        yield self._addAttachment("home2", "calendar2", "2-2.2.ics", "2.2", "attach_2_2.txt")
-        yield self._addAttachmentProperty("home2", "calendar2", "2-2.3.ics", "1.3", "home1", "attach_1_3.txt")
-        yield self._addAttachmentProperty("home2", "calendar3", "2-3.2.ics", "1.4", "home1", "attach_1_4.txt")
-        yield self._addAttachmentProperty("home2", "calendar3", "2-3.3.ics", "1.4", "home1", "attach_1_4.txt")
+        yield self._addAttachment(u"home2", "calendar2", "2-2.2.ics", "2.2", "attach_2_2.txt")
+        yield self._addAttachmentProperty(u"home2", "calendar2", "2-2.3.ics", "1.3", "home1", "attach_1_3.txt")
+        yield self._addAttachmentProperty(u"home2", "calendar3", "2-3.2.ics", "1.4", "home1", "attach_1_4.txt")
+        yield self._addAttachmentProperty(u"home2", "calendar3", "2-3.3.ics", "1.4", "home1", "attach_1_4.txt")
 
 
     @inlineCallbacks
@@ -1586,7 +1586,7 @@
         """
         Test L{txdav.caldav.datastore.sql.DropboxAttachment.convertToManaged} converts properly to a ManagedAttachment.
         """
-        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.2.ics", "1.2", "attach_1_2.txt")
 
         txn = self._sqlCalendarStore.newTransaction()
 
@@ -1616,11 +1616,11 @@
         """
         Test L{txdav.caldav.datastore.sql.ManagedAttachment.newReference} creates a new managed attachment reference.
         """
-        yield self._addAttachment("home1", "calendar1", "1.4.ics", "1.4", "attach_1_4.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.4.ics", "1.4", "attach_1_4.txt")
 
         txn = self._sqlCalendarStore.newTransaction()
 
-        home = (yield txn.calendarHomeWithUID("home1"))
+        home = (yield txn.calendarHomeWithUID(u"home1"))
         calendar = (yield home.calendarWithName("calendar1"))
         event4 = (yield calendar.calendarObjectWithName("1.4.ics"))
         event5 = (yield calendar.calendarObjectWithName("1.5.ics"))
@@ -1664,12 +1664,12 @@
         """
         Test L{txdav.caldav.datastore.sql.CalendarObject.convertAttachments} re-writes calendar data.
         """
-        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_1.txt")
-        yield self._addAttachment("home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_2.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_1.txt")
+        yield self._addAttachment(u"home1", "calendar1", "1.2.ics", "1.2", "attach_1_2_2.txt")
 
         txn = self._sqlCalendarStore.newTransaction()
 
-        home = (yield txn.calendarHomeWithUID("home1"))
+        home = (yield txn.calendarHomeWithUID(u"home1"))
         calendar = (yield home.calendarWithName("calendar1"))
         event = (yield calendar.calendarObjectWithName("1.2.ics"))
 
@@ -1688,7 +1688,7 @@
 
         txn = self._sqlCalendarStore.newTransaction()
 
-        home = (yield txn.calendarHomeWithUID("home1"))
+        home = (yield txn.calendarHomeWithUID(u"home1"))
         calendar = (yield home.calendarWithName("calendar1"))
         event = (yield calendar.calendarObjectWithName("1.2.ics"))
 
@@ -1712,7 +1712,7 @@
 
         # Convert the second dropbox attachment
         txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID("home1"))
+        home = (yield txn.calendarHomeWithUID(u"home1"))
         calendar = (yield home.calendarWithName("calendar1"))
         event = (yield calendar.calendarObjectWithName("1.2.ics"))
         dattachment = (yield DropBoxAttachment.load(txn, "1.2.dropbox", "attach_1_2_2.txt"))
@@ -1722,7 +1722,7 @@
         yield txn.commit()
 
         txn = self._sqlCalendarStore.newTransaction()
-        home = (yield txn.calendarHomeWithUID("home1"))
+        home = (yield txn.calendarHomeWithUID(u"home1"))
         calendar = (yield home.calendarWithName("calendar1"))
         event = (yield calendar.calendarObjectWithName("1.2.ics"))
         component = (yield event.componentForUser()).mainComponent()
@@ -1760,14 +1760,14 @@
         yield calstore._upgradeDropbox(txn, "1.2.dropbox")
         yield txn.commit()
 
-        yield self._verifyConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
-        yield self._verifyNoConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
-        yield self._verifyNoConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
-        yield self._verifyNoConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
-        yield self._verifyNoConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
 
 
     @inlineCallbacks
@@ -1784,14 +1784,14 @@
         yield calstore._upgradeDropbox(txn, "1.3.dropbox")
         yield txn.commit()
 
-        yield self._verifyNoConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
-        yield self._verifyConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
-        yield self._verifyNoConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
-        yield self._verifyConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
-        yield self._verifyNoConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyConversion(u"home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
 
 
     @inlineCallbacks
@@ -1808,14 +1808,14 @@
         yield calstore._upgradeDropbox(txn, "1.4.dropbox")
         yield txn.commit()
 
-        yield self._verifyNoConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
-        yield self._verifyNoConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
-        yield self._verifyConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
-        yield self._verifyConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
-        yield self._verifyNoConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
-        yield self._verifyNoConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
-        yield self._verifyConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
-        yield self._verifyConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyNoConversion(u"home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyNoConversion(u"home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion(u"home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
 
 
     @inlineCallbacks
@@ -1830,14 +1830,14 @@
         calstore = CalendarStoreFeatures(self._sqlCalendarStore)
         yield calstore.upgradeToManagedAttachments(2)
 
-        yield self._verifyConversion("home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
-        yield self._verifyConversion("home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
-        yield self._verifyConversion("home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
-        yield self._verifyConversion("home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
-        yield self._verifyConversion("home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
-        yield self._verifyConversion("home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
-        yield self._verifyConversion("home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
-        yield self._verifyConversion("home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.2.ics", ("attach_1_2_1.txt", "attach_1_2_2.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.4.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home1", "calendar1", "1.5.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home2", "calendar2", "2-2.2.ics", ("attach_2_2.txt",))
+        yield self._verifyConversion(u"home2", "calendar2", "2-2.3.ics", ("attach_1_3.txt",))
+        yield self._verifyConversion(u"home2", "calendar3", "2-3.2.ics", ("attach_1_4.txt",))
+        yield self._verifyConversion(u"home2", "calendar3", "2-3.3.ics", ("attach_1_4.txt",))
 
         # Paths do not exist
         for path in self.paths.values():

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/test/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -16,7 +16,7 @@
 ##
 from twisted.trial.unittest import TestCase
 from twext.python.clsprop import classproperty
-from twisted.internet.defer import inlineCallbacks
+from twisted.internet.defer import inlineCallbacks, succeed
 
 """
 Store test utility functions
@@ -40,7 +40,7 @@
 
 
     def recordWithCalendarUserAddress(self, cuaddr):
-        return self.recordsByCUA.get(cuaddr)
+        return succeed(self.recordsByCUA.get(cuaddr))
 
 
     def addRecord(self, record):
@@ -63,30 +63,46 @@
         cutype="INDIVIDUAL",
         thisServer=True,
         server=None,
-        extras={},
+        associatedAddress=None,
+        streetAddress=None,
+        geographicLocation=None
     ):
 
-        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName, thisServer, server, extras=extras)
+        super(TestCalendarStoreDirectoryRecord, self).__init__(
+            uid, shortNames, fullName, thisServer, server,
+        )
         self.calendarUserAddresses = calendarUserAddresses
         self.cutype = cutype
+        self.associatedAddress = associatedAddress
+        self.streetAddress = streetAddress
+        self.geographicLocation = geographicLocation
 
 
+
     def canonicalCalendarUserAddress(self):
-        cua = ""
-        for candidate in self.calendarUserAddresses:
-            # Pick the first one, but urn:uuid: and mailto: can override
-            if not cua:
-                cua = candidate
-            # But always immediately choose the urn:uuid: form
-            if candidate.startswith("urn:uuid:"):
-                cua = candidate
-                break
-            # Prefer mailto: if no urn:uuid:
-            elif candidate.startswith("mailto:"):
-                cua = candidate
-        return cua
+        """
+            Return a CUA for this record, preferring in this order:
+            urn:uuid: form
+            mailto: form
+            /principals/__uids__/ form
+            first in calendarUserAddresses list (sorted)
+        """
 
+        sortedCuas = sorted(self.calendarUserAddresses)
 
+        for prefix in (
+            "urn:uuid:",
+            "mailto:",
+            "/principals/__uids__/"
+        ):
+            for candidate in sortedCuas:
+                if candidate.startswith(prefix):
+                    return candidate
+
+        # fall back to using the first one
+        return sortedCuas[0]
+
+
     def calendarsEnabled(self):
         return True
 
@@ -109,15 +125,15 @@
 
 
     def canAutoSchedule(self, organizer):
-        return False
+        return succeed(False)
 
 
     def getAutoScheduleMode(self, organizer):
-        return "automatic"
+        return succeed("automatic")
 
 
     def isProxyFor(self, other):
-        return False
+        return succeed(False)
 
 
 
@@ -161,31 +177,23 @@
     # Structured Locations
     directory.addRecord(TestCalendarStoreDirectoryRecord(
         "il1", ("il1",), "1 Infinite Loop", [],
-        extras={
-            "geo" : "37.331741,-122.030333",
-            "streetAddress" : "1 Infinite Loop, Cupertino, CA 95014",
-        }
+        geographicLocation="37.331741,-122.030333",
+        streetAddress="1 Infinite Loop, Cupertino, CA 95014"
     ))
     directory.addRecord(TestCalendarStoreDirectoryRecord(
         "il2", ("il2",), "2 Infinite Loop", [],
-        extras={
-            "geo" : "37.332633,-122.030502",
-            "streetAddress" : "2 Infinite Loop, Cupertino, CA 95014",
-        }
+        geographicLocation="37.332633,-122.030502",
+        streetAddress="2 Infinite Loop, Cupertino, CA 95014"
     ))
     directory.addRecord(TestCalendarStoreDirectoryRecord(
         "room1", ("room1",), "Conference Room One",
         frozenset(("urn:uuid:room1",)),
-        extras={
-            "associatedAddress" : "il1",
-        }
+        associatedAddress="il1",
     ))
     directory.addRecord(TestCalendarStoreDirectoryRecord(
         "room2", ("room2",), "Conference Room Two",
         frozenset(("urn:uuid:room2",)),
-        extras={
-            "associatedAddress" : "il2",
-        }
+        associatedAddress="il2",
     ))
 
     return directory

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/datastore/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -100,35 +100,42 @@
 
 
 
+ at inlineCallbacks
 def normalizationLookup(cuaddr, recordFunction, config):
     """
     Lookup function to be passed to ical.normalizeCalendarUserAddresses.
-    Returns a tuple of (Full name, guid, and calendar user address list)
+    Returns a tuple of (Full name C{str}, guid C{UUID}, and calendar user address list C{str})
     for the given cuaddr.  The recordFunction is called to retrieve the
     record for the cuaddr.
     """
     try:
-        record = recordFunction(cuaddr)
+        record = yield recordFunction(cuaddr)
     except Exception, e:
         log.debug("Lookup of %s failed: %s" % (cuaddr, e))
         record = None
 
     if record is None:
-        return (None, None, None)
+        returnValue((None, None, None))
     else:
+
         # RFC5545 syntax does not allow backslash escaping in
         # parameter values. A double-quote is thus not allowed
         # in a parameter value except as the start/end delimiters.
         # Single quotes are allowed, so we convert any double-quotes
         # to single-quotes.
-        return (
-            record.fullName.replace('"', "'"),
-            record.uid,
-            record.calendarUserAddresses,
+        fullName = record.displayName.replace('"', "'").encode("utf-8")
+        cuas = set(
+            [cua.encode("utf-8") for cua in record.calendarUserAddresses]
         )
+        try:
+            guid = record.guid
+        except AttributeError:
+            guid = None
 
+        returnValue((fullName, guid, cuas))
 
 
+
 @inlineCallbacks
 def dropboxIDFromCalendarObject(calendarObject):
     """

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/icalendardirectoryservice.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/icalendardirectoryservice.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/caldav/icalendardirectoryservice.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -37,8 +37,8 @@
         """
         Return the record for the specified calendar user address.
 
-        @return: the record.
-        @rtype: L{ICalendarStoreDirectoryRecord}
+        @return: Deferred resulting in the record.
+        @rtype: L{Deferred} resulting in L{ICalendarStoreDirectoryRecord}
         """
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/carddav/datastore/query/filter.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/carddav/datastore/query/filter.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/carddav/datastore/query/filter.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -136,7 +136,7 @@
                     return not allof
             return allof
         else:
-            return True
+            return False
 
 
     def valid(self):
@@ -190,9 +190,6 @@
             else:
                 raise ValueError("Unknown child element: %s" % (qname,))
 
-        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
-            raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
-
         if xml_element.qname() == (carddav_namespace, "prop-filter"):
             propfilter_test = xml_element.attributes.get("test", "anyof")
             if propfilter_test not in ("anyof", "allof"):
@@ -200,13 +197,16 @@
         else:
             propfilter_test = "anyof"
 
+        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0) and propfilter_test == "allof":
+            raise ValueError("When test is allof, no other tests allowed when CardDAV:is-not-defined is present")
+
         self.propfilter_test = propfilter_test
         self.qualifier = qualifier
         self.filters = filters
         self.filter_name = xml_element.attributes["name"]
         if isinstance(self.filter_name, unicode):
             self.filter_name = self.filter_name.encode("utf-8")
-        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined) or len(filters)
 
 
     def _deserialize(self, data):
@@ -241,25 +241,18 @@
         matches this filter, False otherwise.
         """
 
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined:
-            return True
+        allof = self.propfilter_test == "allof"
+        if self.qualifier and allof != self.qualifier.match(item):
+            return not allof
 
-        if self.qualifier and not self.qualifier.match(item):
-            return False
-
         if len(self.filters) > 0:
-            allof = self.propfilter_test == "allof"
             for filter in self.filters:
                 if allof != filter._match(item):
                     return not allof
             return allof
         else:
-            return True
+            return not allof
 
-
-
 class PropertyFilter (FilterChildBase):
     """
     Limits a search to specific properties.

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/file.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/file.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -148,6 +148,10 @@
         return self._directoryService
 
 
+    def setDirectoryService(self, directoryService):
+        self._directoryService = directoryService
+
+
     def callWithNewTransactions(self, callback):
         """
         Registers a method to be called whenever a new transaction is
@@ -700,7 +704,7 @@
 
 
     def directoryRecord(self):
-        return self.directoryService().recordWithUID(self.uid())
+        return self.directoryService().recordWithUID(self.uid().decode("utf-8"))
 
 
     def retrieveOldShares(self):

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/conduit.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/conduit.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -76,32 +76,33 @@
         self.store = store
 
 
-    def validRequst(self, source_guid, destination_guid):
+    @inlineCallbacks
+    def validRequest(self, source_uid, destination_uid):
         """
-        Verify that the specified GUIDs are valid for the request and return the
+        Verify that the specified uids are valid for the request and return the
         matching directory records.
 
-        @param source_guid: GUID for the user on whose behalf the request is being made
-        @type source_guid: C{str}
-        @param destination_guid: GUID for the user to whom the request is being sent
-        @type destination_guid: C{str}
+        @param source_uid: UID for the user on whose behalf the request is being made
+        @type source_uid: C{str}
+        @param destination_uid: UID for the user to whom the request is being sent
+        @type destination_uid: C{str}
 
-        @return: C{tuple} of L{IStoreDirectoryRecord}
+        @return: L{Deferred} resulting in C{tuple} of L{IStoreDirectoryRecord}
         """
 
-        source = self.store.directoryService().recordWithUID(source_guid)
+        source = yield self.store.directoryService().recordWithUID(source_uid)
         if source is None:
-            raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
+            raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_uid))
         if not source.thisServer():
-            raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
+            raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_uid))
 
-        destination = self.store.directoryService().recordWithUID(destination_guid)
+        destination = yield self.store.directoryService().recordWithUID(destination_uid)
         if destination is None:
-            raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
+            raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_uid))
         if destination.thisServer():
-            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
+            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_uid))
 
-        return (source, destination,)
+        returnValue((source, destination,))
 
 
     @inlineCallbacks
@@ -166,13 +167,13 @@
 
         @param homeType: Type of home being shared.
         @type homeType: C{int}
-        @param ownerUID: GUID of the sharer.
+        @param ownerUID: UID of the sharer.
         @type ownerUID: C{str}
         @param ownerID: resource ID of the sharer calendar
         @type ownerID: C{int}
         @param ownerName: owner's name of the sharer calendar
         @type ownerName: C{str}
-        @param shareeUID: GUID of the sharee
+        @param shareeUID: UID of the sharee
         @type shareeUID: C{str}
         @param shareUID: Resource/invite ID for sharee
         @type shareUID: C{str}
@@ -186,7 +187,7 @@
         @type supported_components: C{str}
         """
 
-        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
+        _ignore_sender, recipient = yield self.validRequest(ownerUID, shareeUID)
 
         action = {
             "action": "shareinvite",
@@ -250,17 +251,17 @@
 
         @param homeType: Type of home being shared.
         @type homeType: C{int}
-        @param ownerUID: GUID of the sharer.
+        @param ownerUID: UID of the sharer.
         @type ownerUID: C{str}
         @param ownerID: resource ID of the sharer calendar
         @type ownerID: C{int}
-        @param shareeUID: GUID of the sharee
+        @param shareeUID: UID of the sharee
         @type shareeUID: C{str}
         @param shareUID: Resource/invite ID for sharee
         @type shareUID: C{str}
         """
 
-        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
+        _ignore_sender, recipient = yield self.validRequest(ownerUID, shareeUID)
 
         action = {
             "action": "shareuninvite",
@@ -313,9 +314,9 @@
 
         @param homeType: Type of home being shared.
         @type homeType: C{int}
-        @param ownerUID: GUID of the sharer.
+        @param ownerUID: UID of the sharer.
         @type ownerUID: C{str}
-        @param shareeUID: GUID of the recipient
+        @param shareeUID: UID of the recipient
         @type shareeUID: C{str}
         @param shareUID: Resource/invite ID for recipient
         @type shareUID: C{str}
@@ -325,7 +326,7 @@
         @type summary: C{str}
         """
 
-        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
 
         action = {
             "action": "sharereply",
@@ -398,7 +399,7 @@
 
         actionName = "add-attachment"
         shareeView = objectResource._parentCollection
-        action, recipient = self._send(actionName, shareeView, objectResource)
+        action, recipient = yield self._send(actionName, shareeView, objectResource)
         action["rids"] = rids
         action["filename"] = filename
         result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
@@ -458,7 +459,7 @@
 
         actionName = "update-attachment"
         shareeView = objectResource._parentCollection
-        action, recipient = self._send(actionName, shareeView, objectResource)
+        action, recipient = yield self._send(actionName, shareeView, objectResource)
         action["managedID"] = managed_id
         action["filename"] = filename
         result = yield self.sendRequest(shareeView._txn, recipient, action, stream, content_type)
@@ -514,7 +515,7 @@
 
         actionName = "remove-attachment"
         shareeView = objectResource._parentCollection
-        action, recipient = self._send(actionName, shareeView, objectResource)
+        action, recipient = yield self._send(actionName, shareeView, objectResource)
         action["rids"] = rids
         action["managedID"] = managed_id
         result = yield self.sendRequest(shareeView._txn, recipient, action)
@@ -557,6 +558,7 @@
     # Sharer data access related apis
     #
 
+    @inlineCallbacks
     def _send(self, action, parent, child=None):
         """
         Base behavior for an operation on a L{CommonHomeChild}.
@@ -570,7 +572,7 @@
         ownerID = parent.external_id()
         shareeUID = parent.viewerHome().uid()
 
-        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+        _ignore_sender, recipient = yield self.validRequest(shareeUID, ownerUID)
 
         result = {
             "action": action,
@@ -581,7 +583,7 @@
         }
         if child is not None:
             result["resource_id"] = child.id()
-        return result, recipient
+        returnValue((result, recipient))
 
 
     @inlineCallbacks
@@ -644,7 +646,7 @@
         @type kwargs: C{dict}
         """
 
-        action, recipient = self._send(actionName, shareeView, objectResource)
+        action, recipient = yield self._send(actionName, shareeView, objectResource)
         if args is not None:
             action["arguments"] = args
         if kwargs is not None:
@@ -710,7 +712,7 @@
         servertoserver,
         event_details,
     ):
-        action, recipient = self._send("freebusy", calresource)
+        action, recipient = yield self._send("freebusy", calresource)
         action["timerange"] = [timerange.start.getText(), timerange.end.getText()]
         action["matchtotal"] = matchtotal
         action["excludeuid"] = excludeuid

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -88,12 +88,13 @@
         return False
 
 
+    @inlineCallbacks
     def principalForCalendarUserAddress(self, address):
         for principalCollection in self.principalCollections():
-            principal = principalCollection.principalForCalendarUserAddress(address)
+            principal = yield principalCollection.principalForCalendarUserAddress(address)
             if principal is not None:
-                return principal
-        return None
+                returnValue(principal)
+        returnValue(None)
 
 
     def render(self, request):
@@ -179,11 +180,13 @@
             davxml.Privilege(davxml.Read()),
         )
 
-        return davxml.ACL(
-            # DAV:Read for all principals (includes anonymous)
-            davxml.ACE(
-                davxml.Principal(davxml.All()),
-                davxml.Grant(*privs),
-                davxml.Protected(),
-            ),
+        return succeed(
+            davxml.ACL(
+                # DAV:Read for all principals (includes anonymous)
+                davxml.ACE(
+                    davxml.Principal(davxml.All()),
+                    davxml.Grant(*privs),
+                    davxml.Protected(),
+                ),
+            )
         )

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/test/test_conduit.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/podding/test/test_conduit.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -123,29 +123,41 @@
     }
 
 
-    def test_validRequst(self):
+    @inlineCallbacks
+    def test_validRequest(self):
         """
         Cross-pod request fails when there is no shared secret header present.
         """
 
         conduit = PoddingConduit(self.storeUnderTest())
-        r1, r2 = conduit.validRequst("user01", "puser02")
+        r1, r2 = yield conduit.validRequest("user01", "puser02")
         self.assertTrue(r1 is not None)
         self.assertTrue(r2 is not None)
 
-        self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
-        self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
-        self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
+        self.assertFailure(
+            conduit.validRequest("bogus01", "user02"),
+            DirectoryRecordNotFoundError
+        )
 
+        self.assertFailure(
+            conduit.validRequest("user01", "bogus02"),
+            DirectoryRecordNotFoundError
+        )
 
+        self.assertFailure(
+            conduit.validRequest("user01", "user02"),
+            FailedCrossPodRequestError
+        )
 
+
+
 class TestConduitToConduit(MultiStoreConduitTest):
 
     class FakeConduit(PoddingConduit):
 
         @inlineCallbacks
         def send_fake(self, txn, ownerUID, shareeUID):
-            _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+            _ignore_owner, sharee = yield self.validRequest(ownerUID, shareeUID)
             action = {
                 "action": "fake",
                 "echo": "bravo"

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/sql.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/sql.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -211,6 +211,10 @@
         return self._directoryService
 
 
+    def setDirectoryService(self, directoryService):
+        self._directoryService = directoryService
+
+
     def callWithNewTransactions(self, callback):
         """
         Registers a method to be called whenever a new transaction is
@@ -925,102 +929,164 @@
     @classproperty
     def _addGroupQuery(cls):
         gr = schema.GROUPS
-        return Insert({gr.NAME: Parameter("name"),
-                       gr.GROUP_GUID: Parameter("groupGUID"),
-                       gr.MEMBERSHIP_HASH: Parameter("membershipHash")},
-                       Return=gr.GROUP_ID)
+        return Insert(
+            {
+                gr.NAME: Parameter("name"),
+                gr.GROUP_GUID: Parameter("groupUID"),
+                gr.MEMBERSHIP_HASH: Parameter("membershipHash")
+            },
+            Return=gr.GROUP_ID
+        )
 
 
     @classproperty
     def _updateGroupQuery(cls):
         gr = schema.GROUPS
-        return Update({gr.MEMBERSHIP_HASH: Parameter("membershipHash"),
-            gr.NAME: Parameter("name"), gr.MODIFIED: Parameter("timestamp")},
-            Where=(gr.GROUP_GUID == Parameter("groupGUID")))
+        return Update(
+            {
+                gr.MEMBERSHIP_HASH: Parameter("membershipHash"),
+                gr.NAME: Parameter("name"),
+                gr.MODIFIED:
+                Parameter("timestamp")
+            },
+            Where=(gr.GROUP_GUID == Parameter("groupUID"))
+        )
 
 
     @classproperty
-    def _groupByGUID(cls):
+    def _groupByUID(cls):
         gr = schema.GROUPS
-        return Select([gr.GROUP_ID, gr.NAME, gr.MEMBERSHIP_HASH], From=gr,
-                Where=(
-                    gr.GROUP_GUID == Parameter("groupGUID")
-                )
-            )
+        return Select(
+            [gr.GROUP_ID, gr.NAME, gr.MEMBERSHIP_HASH],
+            From=gr,
+            Where=(gr.GROUP_GUID == Parameter("groupUID"))
+        )
 
 
     @classproperty
     def _groupByID(cls):
         gr = schema.GROUPS
-        return Select([gr.GROUP_GUID, gr.NAME, gr.MEMBERSHIP_HASH], From=gr,
-                Where=(
-                    gr.GROUP_ID == Parameter("groupID")
-                )
-            )
+        return Select(
+            [gr.GROUP_GUID, gr.NAME, gr.MEMBERSHIP_HASH],
+            From=gr,
+            Where=(gr.GROUP_ID == Parameter("groupID"))
+        )
 
 
     @classproperty
     def _deleteGroup(cls):
         gr = schema.GROUPS
-        return Delete(From=gr,
-              Where=(gr.GROUP_ID == Parameter("groupID")))
+        return Delete(
+            From=gr,
+            Where=(gr.GROUP_ID == Parameter("groupID"))
+        )
 
 
-    def addGroup(self, groupGUID, name, membershipHash):
+    def addGroup(self, groupUID, name, membershipHash):
         """
-        @type groupGUID: C{UUID}
+        @type groupUID: C{unicode}
+        @type name: C{unicode}
+        @type membershipHash: C{str}
         """
-        return self._addGroupQuery.on(self, name=name,
-            groupGUID=str(groupGUID), membershipHash=membershipHash)
+        return self._addGroupQuery.on(
+            self,
+            name=name.encode("utf-8"),
+            groupUID=groupUID.encode("utf-8"),
+            membershipHash=membershipHash
+        )
 
 
-    def updateGroup(self, groupGUID, name, membershipHash):
+    def updateGroup(self, groupUID, name, membershipHash):
         """
-        @type groupGUID: C{UUID}
+        @type groupUID: C{unicode}
+        @type name: C{unicode}
+        @type membershipHash: C{str}
         """
         timestamp = datetime.datetime.utcnow()
-        return self._updateGroupQuery.on(self, name=name,
-            groupGUID=str(groupGUID), timestamp=timestamp,
-            membershipHash=membershipHash)
+        return self._updateGroupQuery.on(
+            self,
+            name=name.encode("utf-8"),
+            groupUID=groupUID.encode("utf-8"),
+            timestamp=timestamp,
+            membershipHash=membershipHash
+        )
 
 
     @inlineCallbacks
-    def groupByGUID(self, groupGUID):
+    def groupByUID(self, groupUID):
         """
-        @type groupGUID: C{UUID}
+        Return or create a record for the group UID.
+
+        @type groupUID: C{unicode}
+
+        @return: Deferred firing with tuple of group ID C{str}, group name
+            C{unicode}, and membership hash C{str}
         """
-        results = (yield self._groupByGUID.on(self, groupGUID=str(groupGUID)))
+        results = (
+            yield self._groupByUID.on(
+                self, groupUID=groupUID.encode("utf-8")
+            )
+        )
         if results:
-            returnValue(results[0])
+            returnValue((
+                results[0][0],  # group id
+                results[0][1].decode("utf-8"),  # name
+                results[0][2],  # membership hash
+            ))
         else:
-            savepoint = SavepointAction("groupByGUID")
+            savepoint = SavepointAction("groupByUID")
             yield savepoint.acquire(self)
             try:
-                yield self.addGroup(groupGUID, "", "")
+                yield self.addGroup(groupUID, u"", "")
             except Exception:
                 yield savepoint.rollback(self)
-                results = (yield self._groupByGUID.on(self,
-                    groupGUID=str(groupGUID)))
+                results = (
+                    yield self._groupByUID.on(
+                        self, groupUID=groupUID.encode("utf-8")
+                    )
+                )
                 if results:
-                    returnValue(results[0])
+                    returnValue((
+                        results[0][0],  # group id
+                        results[0][1].decode("utf-8"),  # name
+                        results[0][2],  # membership hash
+                    ))
                 else:
                     raise
             else:
                 yield savepoint.release(self)
-                results = (yield self._groupByGUID.on(self,
-                    groupGUID=str(groupGUID)))
+                results = (
+                    yield self._groupByUID.on(
+                        self, groupUID=groupUID.encode("utf-8")
+                    )
+                )
                 if results:
-                    returnValue(results[0])
+                    returnValue((
+                        results[0][0],  # group id
+                        results[0][1].decode("utf-8"),  # name
+                        results[0][2],  # membership hash
+                    ))
                 else:
                     raise
 
 
     @inlineCallbacks
     def groupByID(self, groupID):
+        """
+        Given a group ID, return the group UID, or raise NotFoundError
+
+        @type groupID: C{str}
+        @return: Deferred firing with a tuple of group UID C{unicode},
+            group name C{unicode}, and membership hash C{str}
+        """
         try:
             results = (yield self._groupByID.on(self, groupID=groupID))[0]
             if results:
-                results = [UUID("urn:uuid:" + results[0])] + results[1:]
+                results = (
+                    results[0].decode("utf-8"),
+                    results[1].decode("utf-8"),
+                    results[2]
+                )
             returnValue(results)
         except IndexError:
             raise NotFoundError
@@ -1040,7 +1106,7 @@
         return Insert(
             {
                 gm.GROUP_ID: Parameter("groupID"),
-                gm.MEMBER_GUID: Parameter("memberGUID")
+                gm.MEMBER_GUID: Parameter("memberUID")
             }
         )
 
@@ -1053,7 +1119,7 @@
             Where=(
                 gm.GROUP_ID == Parameter("groupID")
             ).And(
-                gm.MEMBER_GUID == Parameter("memberGUID")
+                gm.MEMBER_GUID == Parameter("memberUID")
             )
         )
 
@@ -1072,25 +1138,35 @@
 
     @classproperty
     def _selectGroupsForQuery(cls):
+        gr = schema.GROUPS
         gm = schema.GROUP_MEMBERSHIP
+
         return Select(
-            [gm.GROUP_ID],
-            From=gm,
+            [gr.GROUP_GUID],
+            From=gr,
             Where=(
-                gm.MEMBER_GUID == Parameter("guid")
+                gr.GROUP_ID.In(
+                    Select(
+                        [gm.GROUP_ID],
+                        From=gm,
+                        Where=(
+                            gm.MEMBER_GUID == Parameter("uid")
+                        )
+                    )
+                )
             )
         )
 
 
-    def addMemberToGroup(self, memberGUID, groupID):
+    def addMemberToGroup(self, memberUID, groupID):
         return self._addMemberToGroupQuery.on(
-            self, groupID=groupID, memberGUID=str(memberGUID)
+            self, groupID=groupID, memberUID=memberUID.encode("utf-8")
         )
 
 
-    def removeMemberFromGroup(self, memberGUID, groupID):
+    def removeMemberFromGroup(self, memberUID, groupID):
         return self._removeMemberFromGroupQuery.on(
-            self, groupID=groupID, memberGUID=str(memberGUID)
+            self, groupID=groupID, memberUID=memberUID.encode("utf-8")
         )
 
 
@@ -1110,25 +1186,29 @@
         members = set()
         results = (yield self._selectGroupMembersQuery.on(self, groupID=groupID))
         for row in results:
-            members.add(UUID("urn:uuid:" + row[0]))
+            members.add(row[0].decode("utf-8"))
         returnValue(members)
 
 
     @inlineCallbacks
-    def groupsFor(self, guid):
+    def groupsFor(self, uid):
         """
-        Returns the cached set of GUIDs for the groups this given guid is
+        Returns the cached set of UIDs for the groups this given uid is
         a member of.
 
-        @param guid: the guid
-        @type guid: C{UUID}
+        @param uid: the uid
+        @type uid: C{unicode}
         @return: the set of group IDs
         @rtype: a Deferred which fires with a set() of C{int} group IDs
         """
         groups = set()
-        results = (yield self._selectGroupsForQuery.on(self, guid=str(guid)))
+        results = (
+            yield self._selectGroupsForQuery.on(
+                self, uid=uid.encode("utf-8")
+            )
+        )
         for row in results:
-            groups.add(row[0])
+            groups.add(row[0].decode("utf-8"))
         returnValue(groups)
 
     # End of Group Members
@@ -1171,6 +1251,19 @@
 
 
     @classproperty
+    def _removeDelegatesQuery(cls):
+        de = schema.DELEGATES
+        return Delete(
+            From=de,
+            Where=(
+                de.DELEGATOR == Parameter("delegator")
+            ).And(
+                de.READ_WRITE == Parameter("readWrite")
+            )
+        )
+
+
+    @classproperty
     def _removeDelegateGroupQuery(cls):
         ds = schema.DELEGATE_GROUPS
         return Delete(
@@ -1186,6 +1279,19 @@
 
 
     @classproperty
+    def _removeDelegateGroupsQuery(cls):
+        ds = schema.DELEGATE_GROUPS
+        return Delete(
+            From=ds,
+            Where=(
+                ds.DELEGATOR == Parameter("delegator")
+            ).And(
+                ds.READ_WRITE == Parameter("readWrite")
+            )
+        )
+
+
+    @classproperty
     def _selectDelegatesQuery(cls):
         de = schema.DELEGATES
         return Select(
@@ -1202,13 +1308,23 @@
     @classproperty
     def _selectDelegateGroupsQuery(cls):
         ds = schema.DELEGATE_GROUPS
+        gr = schema.GROUPS
+
         return Select(
-            [ds.GROUP_ID],
-            From=ds,
+            [gr.GROUP_GUID],
+            From=gr,
             Where=(
-                ds.DELEGATOR == Parameter("delegator")
-            ).And(
-                ds.READ_WRITE == Parameter("readWrite")
+                gr.GROUP_ID.In(
+                    Select(
+                        [ds.GROUP_ID],
+                        From=ds,
+                        Where=(
+                            ds.DELEGATOR == Parameter("delegator")
+                        ).And(
+                            ds.READ_WRITE == Parameter("readWrite")
+                        )
+                    )
+                )
             )
         )
 
@@ -1320,18 +1436,18 @@
         Adds a row to the DELEGATES table.  The delegate should not be a
         group.  To delegate to a group, call addDelegateGroup() instead.
 
-        @param delegator: the GUID of the delegator
-        @type delegator: C{UUID}
-        @param delegate: the GUID of the delegate
-        @type delegate: C{UUID}
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
+        @param delegate: the UID of the delegate
+        @type delegate: C{unicode}
         @param readWrite: grant read and write access if True, otherwise
             read-only access
         @type readWrite: C{boolean}
         """
         return self._addDelegateQuery.on(
             self,
-            delegator=str(delegator),
-            delegate=str(delegate),
+            delegator=delegator.encode("utf-8"),
+            delegate=delegate.encode("utf-8"),
             readWrite=1 if readWrite else 0
         )
 
@@ -1342,8 +1458,8 @@
         Adds a row to the DELEGATE_GROUPS table.  The delegate should be a
         group.  To delegate to a person, call addDelegate() instead.
 
-        @param delegator: the GUID of the delegator
-        @type delegator: C{UUID}
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
         @param delegateGroupID: the GROUP_ID of the delegate group
         @type delegateGroupID: C{int}
         @param readWrite: grant read and write access if True, otherwise
@@ -1352,7 +1468,7 @@
         """
         return self._addDelegateGroupQuery.on(
             self,
-            delegator=str(delegator),
+            delegator=delegator.encode("utf-8"),
             groupID=delegateGroupID,
             readWrite=1 if readWrite else 0,
             isExternal=1 if isExternal else 0
@@ -1364,29 +1480,47 @@
         Removes a row from the DELEGATES table.  The delegate should not be a
         group.  To remove a delegate group, call removeDelegateGroup() instead.
 
-        @param delegator: the GUID of the delegator
-        @type delegator: C{UUID}
-        @param delegate: the GUID of the delegate
-        @type delegate: C{UUID}
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
+        @param delegate: the UID of the delegate
+        @type delegate: C{unicode}
         @param readWrite: remove read and write access if True, otherwise
             read-only access
         @type readWrite: C{boolean}
         """
         return self._removeDelegateQuery.on(
             self,
-            delegator=str(delegator),
-            delegate=str(delegate),
+            delegator=delegator.encode("utf-8"),
+            delegate=delegate.encode("utf-8"),
             readWrite=1 if readWrite else 0
         )
 
 
+    def removeDelegates(self, delegator, readWrite):
+        """
+        Removes all rows for this delegator/readWrite combination from the
+        DELEGATES table.
+
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
+        @param readWrite: remove read and write access if True, otherwise
+            read-only access
+        @type readWrite: C{boolean}
+        """
+        return self._removeDelegatesQuery.on(
+            self,
+            delegator=delegator.encode("utf-8"),
+            readWrite=1 if readWrite else 0
+        )
+
+
     def removeDelegateGroup(self, delegator, delegateGroupID, readWrite):
         """
         Removes a row from the DELEGATE_GROUPS table.  The delegate should be a
         group.  To remove a delegate person, call removeDelegate() instead.
 
-        @param delegator: the GUID of the delegator
-        @type delegator: C{UUID}
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
         @param delegateGroupID: the GROUP_ID of the delegate group
         @type delegateGroupID: C{int}
         @param readWrite: remove read and write access if True, otherwise
@@ -1395,26 +1529,44 @@
         """
         return self._removeDelegateGroupQuery.on(
             self,
-            delegator=str(delegator),
+            delegator=delegator.encode("utf-8"),
             groupID=delegateGroupID,
             readWrite=1 if readWrite else 0
         )
 
 
+    def removeDelegateGroups(self, delegator, readWrite):
+        """
+        Removes all rows for this delegator/readWrite combination from the
+        DELEGATE_GROUPS table.
+
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
+        @param readWrite: remove read and write access if True, otherwise
+            read-only access
+        @type readWrite: C{boolean}
+        """
+        return self._removeDelegateGroupsQuery.on(
+            self,
+            delegator=delegator.encode("utf-8"),
+            readWrite=1 if readWrite else 0
+        )
+
+
     @inlineCallbacks
-    def delegates(self, delegator, readWrite):
+    def delegates(self, delegator, readWrite, expanded=False):
         """
-        Returns the GUIDs of all delegates for the given delegator.  If
-        delegate access was granted to any groups, those groups' members
-        (flattened) will be included. No GUIDs of the groups themselves
-        will be returned.
+        Returns the UIDs of all delegates for the given delegator.  If
+        expanded is False, only the direct delegates (users and groups)
+        are returned.  If expanded is True, the expanded membmership is
+        returned, not including the groups themselves.
 
-        @param delegator: the GUID of the delegator
-        @type delegator: C{UUID}
+        @param delegator: the UID of the delegator
+        @type delegator: C{unicode}
         @param readWrite: the access-type to check for; read and write
             access if True, otherwise read-only access
         @type readWrite: C{boolean}
-        @returns: the GUIDs of the delegates (for the specified access
+        @returns: the UIDs of the delegates (for the specified access
             type)
         @rtype: a Deferred resulting in a set
         """
@@ -1424,35 +1576,48 @@
         results = (
             yield self._selectDelegatesQuery.on(
                 self,
-                delegator=str(delegator),
+                delegator=delegator.encode("utf-8"),
                 readWrite=1 if readWrite else 0
             )
         )
         for row in results:
-            delegates.add(UUID("urn:uuid:" + row[0]))
+            delegates.add(row[0].decode("utf-8"))
 
-        # Finally get those who are in groups which have been delegated to
-        results = (
-            yield self._selectIndirectDelegatesQuery.on(
-                self,
-                delegator=str(delegator),
-                readWrite=1 if readWrite else 0
+        if expanded:
+            # Get those who are in groups which have been delegated to
+            results = (
+                yield self._selectIndirectDelegatesQuery.on(
+                    self,
+                    delegator=delegator.encode("utf-8"),
+                    readWrite=1 if readWrite else 0
+                )
             )
-        )
-        for row in results:
-            delegates.add(UUID("urn:uuid:" + row[0]))
+            for row in results:
+                delegates.add(row[0].decode("utf-8"))
 
+        else:
+            # Get the directly-delegated-to groups
+            results = (
+                yield self._selectDelegateGroupsQuery.on(
+                    self,
+                    delegator=delegator.encode("utf-8"),
+                    readWrite=1 if readWrite else 0
+                )
+            )
+            for row in results:
+                delegates.add(row[0].decode("utf-8"))
+
         returnValue(delegates)
 
 
     @inlineCallbacks
     def delegators(self, delegate, readWrite):
         """
-        Returns the GUIDs of all delegators which have granted access to
+        Returns the UIDs of all delegators which have granted access to
         the given delegate, either directly or indirectly via groups.
 
-        @param delegate: the GUID of the delegate
-        @type delegate: C{UUID}
+        @param delegate: the UID of the delegate
+        @type delegate: C{unicode}
         @param readWrite: the access-type to check for; read and write
             access if True, otherwise read-only access
         @type readWrite: C{boolean}
@@ -1466,24 +1631,24 @@
         results = (
             yield self._selectDirectDelegatorsQuery.on(
                 self,
-                delegate=str(delegate),
+                delegate=delegate.encode("utf-8"),
                 readWrite=1 if readWrite else 0
             )
         )
         for row in results:
-            delegators.add(UUID("urn:uuid:" + row[0]))
+            delegators.add(row[0].decode("utf-8"))
 
         # Finally get those who have delegated to groups the delegate
         # is a member of
         results = (
             yield self._selectIndirectDelegatorsQuery.on(
                 self,
-                delegate=str(delegate),
+                delegate=delegate.encode("utf-8"),
                 readWrite=1 if readWrite else 0
             )
         )
         for row in results:
-            delegators.add(UUID("urn:uuid:" + row[0]))
+            delegators.add(row[0].decode("utf-8"))
 
         returnValue(delegators)
 
@@ -1491,11 +1656,11 @@
     @inlineCallbacks
     def allGroupDelegates(self):
         """
-        Return the GUIDs of all groups which have been delegated to.  Useful
+        Return the UIDs of all groups which have been delegated to.  Useful
         for obtaining the set of groups which need to be synchronized from
         the directory.
 
-        @returns: the GUIDs of all delegated-to groups
+        @returns: the UIDs of all delegated-to groups
         @rtype: a Deferred resulting in a set
         """
         gr = schema.GROUPS
@@ -1508,7 +1673,7 @@
         ).on(self))
         delegates = set()
         for row in results:
-            delegates.add(UUID("urn:uuid:" + row[0]))
+            delegates.add(row[0].decode("utf-8"))
 
         returnValue(delegates)
 
@@ -1516,22 +1681,22 @@
     @inlineCallbacks
     def externalDelegates(self):
         """
-        Returns a dictionary mapping delegate GUIDs to (read-group, write-group)
+        Returns a dictionary mapping delegate UIDs to (read-group, write-group)
         tuples, including only those assignments that originated from the
         directory.
 
-        @returns: dictionary mapping delegator guid to (readDelegateGUID,
-            writeDelegateGUID) tuples
+        @returns: dictionary mapping delegator uid to (readDelegateUID,
+            writeDelegateUID) tuples
         @rtype: a Deferred resulting in a dictionary
         """
         delegates = {}
 
         # Get the externally managed delegates (which are all groups)
         results = (yield self._selectExternalDelegateGroupsQuery.on(self))
-        for delegator, readDelegateGUID, writeDelegateGUID in results:
-            delegates[UUID(delegator)] = (
-                UUID(readDelegateGUID) if readDelegateGUID else None,
-                UUID(writeDelegateGUID) if writeDelegateGUID else None
+        for delegator, readDelegateUID, writeDelegateUID in results:
+            delegates[delegator.encode("utf-8")] = (
+                readDelegateUID.encode("utf-8") if readDelegateUID else None,
+                writeDelegateUID.encode("utf-8") if writeDelegateUID else None
             )
 
         returnValue(delegates)
@@ -1540,7 +1705,7 @@
     @inlineCallbacks
     def assignExternalDelegates(
         self, delegator, readDelegateGroupID, writeDelegateGroupID,
-        readDelegateGUID, writeDelegateGUID
+        readDelegateUID, writeDelegateUID
     ):
         """
         Update the external delegate group table so we can quickly identify
@@ -1563,12 +1728,12 @@
         )
 
         # Store new assignments in the external comparison table
-        if readDelegateGUID or writeDelegateGUID:
+        if readDelegateUID or writeDelegateUID:
             readDelegateForDB = (
-                str(readDelegateGUID) if readDelegateGUID else ""
+                readDelegateUID.encode("utf-8") if readDelegateUID else ""
             )
             writeDelegateForDB = (
-                str(writeDelegateGUID) if writeDelegateGUID else ""
+                writeDelegateUID.encode("utf-8") if writeDelegateUID else ""
             )
             yield self._storeExternalDelegateGroupsPairQuery.on(
                 self,
@@ -2745,6 +2910,9 @@
     @classmethod
     @inlineCallbacks
     def homeWithUID(cls, txn, uid, create=False):
+        """
+        @param uid: I'm going to assume uid is utf-8 encoded bytes
+        """
         homeObject = yield cls.makeClass(txn, uid)
         if homeObject is not None:
             returnValue(homeObject)
@@ -2753,7 +2921,7 @@
                 returnValue(None)
 
             # Determine if the user is local or external
-            record = txn.directoryService().recordWithUID(uid)
+            record = yield txn.directoryService().recordWithUID(uid.decode("utf-8"))
             if record is None:
                 raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
 
@@ -2847,7 +3015,7 @@
 
 
     def directoryRecord(self):
-        return self.directoryService().recordWithUID(self.uid())
+        return self.directoryService().recordWithUID(self.uid().decode("utf-8"))
 
 
     @inlineCallbacks
@@ -6748,6 +6916,9 @@
     @classmethod
     @inlineCallbacks
     def notificationsWithUID(cls, txn, uid, create):
+        """
+        @param uid: I'm going to assume uid is utf-8 encoded bytes
+        """
         rows = yield cls._resourceIDFromUIDQuery.on(txn, uid=uid)
 
         if rows:
@@ -6755,7 +6926,7 @@
             created = False
         elif create:
             # Determine if the user is local or external
-            record = txn.directoryService().recordWithUID(uid)
+            record = yield txn.directoryService().recordWithUID(uid.decode("utf-8"))
             if record is None:
                 raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
 

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/test/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/common/datastore/test/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -46,7 +46,7 @@
 from twisted.application.service import Service
 from twisted.internet import reactor
 from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.defer import returnValue
+from twisted.internet.defer import returnValue, succeed
 from twisted.internet.task import deferLater
 from twisted.trial.unittest import TestCase
 
@@ -110,14 +110,14 @@
 
 
     def recordWithUID(self, uid):
-        return self.records.get(uid)
+        return succeed(self.records.get(uid))
 
 
     def recordWithGUID(self, guid):
         for record in self.records.itervalues():
             if record.guid == guid:
-                return record
-        return None
+                return succeed(record)
+        return succeed(None)
 
 
     def addRecord(self, record):

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/dps/client.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/dps/client.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/dps/client.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -15,53 +15,90 @@
 ##
 
 import cPickle as pickle
+import uuid
 
 from twext.python.log import Logger
 from twext.who.directory import DirectoryRecord as BaseDirectoryRecord
 from twext.who.directory import DirectoryService as BaseDirectoryService
-from twext.who.idirectory import RecordType
+from twext.who.expression import Operand
+from twext.who.idirectory import RecordType, IDirectoryService
 import twext.who.idirectory
 from twext.who.util import ConstantsContainer
 from twisted.internet import reactor
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.protocol import ClientCreator
 from twisted.protocols import amp
+from twisted.python.constants import Names, NamedConstant
+from txdav.caldav.icalendardirectoryservice import (
+    ICalendarStoreDirectoryRecord
+)
+from txdav.common.idirectoryservice import IStoreDirectoryService
 from txdav.dps.commands import (
     RecordWithShortNameCommand, RecordWithUIDCommand, RecordWithGUIDCommand,
     RecordsWithRecordTypeCommand, RecordsWithEmailAddressCommand,
+    RecordsMatchingTokensCommand, RecordsMatchingFieldsCommand,
+    MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand
 )
+from txdav.who.directory import (
+    CalendarDirectoryRecordMixin, CalendarDirectoryServiceMixin
+)
+import txdav.who.augment
+import txdav.who.delegates
 import txdav.who.idirectory
+import txdav.who.wiki
 from zope.interface import implementer
 
-
 log = Logger()
 
-
 ##
 ## Client implementation of Directory Proxy Service
 ##
 
 
- at implementer(twext.who.idirectory.IDirectoryService)
-class DirectoryService(BaseDirectoryService):
+
+## MOVE2WHO TODOs:
+## SACLs
+## LDAP
+## Tests from old twistedcaldav/directory
+## Cmd line tools
+## Store based directory service (records in the store, i.e.
+##    locations/resources)
+## Separate store for DPS (augments and delegates separate from calendar data)
+## Store autoAcceptGroups in the group db?
+
+ at implementer(IDirectoryService, IStoreDirectoryService)
+class DirectoryService(BaseDirectoryService, CalendarDirectoryServiceMixin):
     """
     Client side of directory proxy
     """
 
+    # FIXME: somehow these should come from the actual directory:
+
     recordType = ConstantsContainer(
         (twext.who.idirectory.RecordType,
-         txdav.who.idirectory.RecordType)
+         txdav.who.idirectory.RecordType,
+         txdav.who.delegates.RecordType,
+         txdav.who.wiki.RecordType)
     )
 
+    fieldName = ConstantsContainer(
+        (twext.who.idirectory.FieldName,
+         txdav.who.idirectory.FieldName,
+         txdav.who.augment.FieldName)
+    )
 
+
     def _dictToRecord(self, serializedFields):
         """
-        This to be replaced by something awesome
+        Turn a dictionary of fields sent from the server into a directory
+        record
         """
         if not serializedFields:
             return None
 
+        # print("FIELDS", serializedFields)
+
         fields = {}
         for fieldName, value in serializedFields.iteritems():
             try:
@@ -70,8 +107,21 @@
                 # unknown field
                 pass
             else:
-                fields[field] = value
-        fields[self.fieldName.recordType] = self.recordType.user
+                valueType = self.fieldName.valueType(field)
+                if valueType in (unicode, bool):
+                    fields[field] = value
+                elif valueType is uuid.UUID:
+                    fields[field] = uuid.UUID(value)
+                elif issubclass(valueType, Names):
+                    if value is not None:
+                        fields[field] = field.valueType.lookupByName(value)
+                    else:
+                        fields[field] = None
+                elif issubclass(valueType, NamedConstant):
+                    if fieldName == "recordType":  # Is there a better way?
+                        fields[field] = self.recordType.lookupByName(value)
+
+        # print("AFTER:", fields)
         return DirectoryRecord(self, fields)
 
 
@@ -92,14 +142,17 @@
 
     @inlineCallbacks
     def _getConnection(self):
-        # TODO: make socket patch configurable
         # TODO: reconnect if needed
 
-        # path = config.DirectoryProxy.SocketPath
-        path = "data/Logs/state/directory-proxy.sock"
+        # FIXME:
+        from twistedcaldav.config import config
+        path = config.DirectoryProxy.SocketPath
+        # path = "data/Logs/state/directory-proxy.sock"
         if getattr(self, "_connection", None) is None:
             log.debug("Creating connection")
-            connection = (yield ClientCreator(reactor, amp.AMP).connectUNIX(path))
+            connection = (
+                yield ClientCreator(reactor, amp.AMP).connectUNIX(path)
+            )
             self._connection = connection
         else:
             log.debug("Already have connection")
@@ -127,15 +180,31 @@
 
 
     def recordWithShortName(self, recordType, shortName):
+        # MOVE2WHO
+        # temporary hack until we can fix all callers not to pass strings:
+        if isinstance(recordType, (str, unicode)):
+            recordType = self.recordType.lookupByName(recordType)
+
+        # MOVE2WHO, REMOVE THIS HACK TOO:
+        if not isinstance(shortName, unicode):
+            # log.warn("Need to change shortName to unicode")
+            shortName = shortName.decode("utf-8")
+
+
         return self._call(
             RecordWithShortNameCommand,
             self._processSingleRecord,
-            recordType=recordType.description.encode("utf-8"),
+            recordType=recordType.name.encode("utf-8"),
             shortName=shortName.encode("utf-8")
         )
 
 
     def recordWithUID(self, uid):
+        # MOVE2WHO, REMOVE THIS:
+        if not isinstance(uid, unicode):
+            # log.warn("Need to change uid to unicode")
+            uid = uid.decode("utf-8")
+
         return self._call(
             RecordWithUIDCommand,
             self._processSingleRecord,
@@ -147,7 +216,7 @@
         return self._call(
             RecordWithGUIDCommand,
             self._processSingleRecord,
-            guid=guid.encode("utf-8")
+            guid=str(guid)
         )
 
 
@@ -155,7 +224,7 @@
         return self._call(
             RecordsWithRecordTypeCommand,
             self._processMultipleRecords,
-            recordType=recordType.description.encode("utf-8")
+            recordType=recordType.name.encode("utf-8")
         )
 
 
@@ -163,13 +232,62 @@
         return self._call(
             RecordsWithEmailAddressCommand,
             self._processMultipleRecords,
-            emailAddress=emailAddress
+            emailAddress=emailAddress.encode("utf-8")
         )
 
 
+    def recordsMatchingTokens(
+        self, tokens, context=None, limitResults=50, timeoutSeconds=10
+    ):
+        return self._call(
+            RecordsMatchingTokensCommand,
+            self._processMultipleRecords,
+            tokens=[t.encode("utf-8") for t in tokens],
+            context=context
+        )
 
-class DirectoryRecord(BaseDirectoryRecord):
 
+    def recordsMatchingFields(
+        self, fields, operand=Operand.OR, recordType=None
+    ):
+        newFields = []
+        for fieldName, searchTerm, matchFlags, matchType in fields:
+
+            if isinstance(searchTerm, uuid.UUID):
+                searchTerm = unicode(searchTerm)
+
+            newFields.append(
+                (
+                    fieldName.encode("utf-8"),
+                    searchTerm.encode("utf-8"),
+                    matchFlags.name.encode("utf-8"),
+                    matchType.name.encode("utf-8")
+                )
+            )
+        if recordType is not None:
+            recordType = recordType.name.encode("utf-8")
+
+        return self._call(
+            RecordsMatchingFieldsCommand,
+            self._processMultipleRecords,
+            fields=newFields,
+            operand=operand.name.encode("utf-8"),
+            recordType=recordType
+        )
+
+
+    def recordsFromExpression(self, expression):
+        raise NotImplementedError(
+            "This won't work until expressions are serializable to send "
+            "across AMP"
+        )
+
+
+
+ at implementer(ICalendarStoreDirectoryRecord)
+class DirectoryRecord(BaseDirectoryRecord, CalendarDirectoryRecordMixin):
+
+
     def verifyPlaintextPassword(self, password):
         return self.service._call(
             VerifyPlaintextPasswordCommand,
@@ -201,6 +319,35 @@
 
 
 
+    def members(self):
+        return self.service._call(
+            MembersCommand,
+            self.service._processMultipleRecords,
+            uid=self.uid.encode("utf-8")
+        )
+
+
+    def groups(self):
+        return self.service._call(
+            GroupsCommand,
+            self.service._processMultipleRecords,
+            uid=self.uid.encode("utf-8")
+        )
+
+
+    def setMembers(self, members):
+        log.debug("DPS Client setMembers")
+        memberUIDs = [m.uid.encode("utf-8") for m in members]
+        return self.service._call(
+            SetMembersCommand,
+            lambda x: x['success'],
+            uid=self.uid.encode("utf-8"),
+            memberUIDs=memberUIDs
+        )
+
+
+
+
 # Test client:
 
 
@@ -212,22 +359,28 @@
     if record:
         authenticated = (yield record.verifyPlaintextPassword("negas"))
         print("plain auth: {a}".format(a=authenticated))
-    """
-    record = (yield ds.recordWithUID("__dre__"))
-    print("uid: {r}".format(r=record))
-    if record:
-        authenticated = (yield record.verifyPlaintextPassword("erd"))
-        print("plain auth: {a}".format(a=authenticated))
-    record = (yield ds.recordWithGUID("A3B1158F-0564-4F5B-81E4-A89EA5FF81B0"))
-    print("guid: {r}".format(r=record))
-    records = (yield ds.recordsWithRecordType(RecordType.user))
-    print("recordType: {r}".format(r=records))
-    records = (yield ds.recordsWithEmailAddress("cdaboo at bitbucket.calendarserver.org"))
-    print("emailAddress: {r}".format(r=records))
-    """
 
+    # record = (yield ds.recordWithUID("__dre__"))
+    # print("uid: {r}".format(r=record))
+    # if record:
+    #     authenticated = (yield record.verifyPlaintextPassword("erd"))
+    #     print("plain auth: {a}".format(a=authenticated))
 
+    # record = yield ds.recordWithGUID(
+    #     "A3B1158F-0564-4F5B-81E4-A89EA5FF81B0"
+    # )
+    # print("guid: {r}".format(r=record))
 
+    # records = yield ds.recordsWithRecordType(RecordType.user)
+    # print("recordType: {r}".format(r=records))
+
+    # records = yield ds.recordsWithEmailAddress(
+    #     "cdaboo at bitbucket.calendarserver.org"
+    # )
+    # print("emailAddress: {r}".format(r=records))
+
+
+
 def succeeded(result):
     print("yay")
     reactor.stop()

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/dps/commands.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/dps/commands.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/dps/commands.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -67,7 +67,27 @@
     ]
 
 
+class RecordsMatchingTokensCommand(amp.Command):
+    arguments = [
+        ('tokens', amp.ListOf(amp.String())),
+        ('context', amp.String(optional=True)),
+    ]
+    response = [
+        ('fieldsList', amp.String()),
+    ]
 
+
+class RecordsMatchingFieldsCommand(amp.Command):
+    arguments = [
+        ('fields', amp.ListOf(amp.ListOf(amp.String()))),
+        ('operand', amp.String()),
+        ('recordType', amp.String(optional=True)),
+    ]
+    response = [
+        ('fieldsList', amp.String()),
+    ]
+
+
 class UpdateRecordsCommand(amp.Command):
     arguments = [
         ('fieldsList', amp.String()),
@@ -89,6 +109,37 @@
 
 
 
+class MembersCommand(amp.Command):
+    arguments = [
+        ('uid', amp.String()),
+    ]
+    response = [
+        ('fieldsList', amp.String()),
+    ]
+
+
+
+class GroupsCommand(amp.Command):
+    arguments = [
+        ('uid', amp.String()),
+    ]
+    response = [
+        ('fieldsList', amp.String()),
+    ]
+
+
+
+class SetMembersCommand(amp.Command):
+    arguments = [
+        ('uid', amp.String()),
+        ('memberUIDs', amp.ListOf(amp.String())),
+    ]
+    response = [
+        ('success', amp.Boolean()),
+    ]
+
+
+
 class VerifyPlaintextPasswordCommand(amp.Command):
     arguments = [
         ('uid', amp.String()),

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/dps/json.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/dps/json.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/dps/json.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/dps/json.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,149 @@
+##
+# Copyright (c) 2014 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 absolute_import
+
+"""
+JSON serialization utilities.
+"""
+
+__all__ = [
+    "expressionAsJSONText",
+    "expressionFromJSONText",
+]
+
+from json import dumps, loads as from_json_text
+
+from twext.who.expression import (
+    CompoundExpression, Operand,
+    MatchExpression, MatchType, MatchFlags,
+)
+
+
+
+def expressionAsJSONText(expression):
+    json = expressionAsJSON(expression)
+    return to_json_text(json)
+
+
+def expressionAsJSON(expression):
+    if isinstance(expression, CompoundExpression):
+        return compoundExpressionAsJSON(expression)
+
+    if isinstance(expression, MatchExpression):
+        return matchExpressionAsJSON(expression)
+
+    raise NotImplementedError(
+        "Unknown expression type: {!r}".format(expression)
+    )
+
+
+def compoundExpressionAsJSON(expression):
+    return dict(
+        type=expression.__class__.__name__,
+        operand=expression.operand.name,
+        expressions=[expressionAsJSON(e) for e in expression.expressions],
+    )
+
+
+def matchExpressionAsJSON(expression):
+    return dict(
+        type=expression.__class__.__name__,
+        field=expression.fieldName.name,
+        value=expression.fieldValue,
+        match=expression.matchType.name,
+        flags=expression.flags.name,
+    )
+    raise NotImplementedError()
+
+
+def expressionFromJSONText(jsonText):
+    json = from_json_text(jsonText)
+    return expressionFromJSON(json)
+
+
+def expressionFromJSON(json):
+    if not isinstance(json, dict):
+        raise TypeError("JSON expression must be a dict.")
+
+    try:
+        json_type = json["type"]
+    except KeyError as e:
+        raise ValueError("JSON expression must have {!r} key.".format(e[0]))
+
+    if json_type == "CompoundExpression":
+        return compoundExpressionFromJSON(json)
+
+    if json_type == "MatchExpression":
+        return matchExpressionFromJSON(json)
+
+    raise NotImplementedError(
+        "Unknown expression type: {}".format(json_type)
+    )
+
+
+def compoundExpressionFromJSON(json):
+    try:
+        expressions_json = json["expressions"]
+        operand_json = json["operand"]
+    except KeyError as e:
+        raise ValueError(
+            "JSON compound expression must have {!r} key.".format(e[0])
+        )
+
+    expressions = tuple(expressionFromJSON(e) for e in expressions_json)
+    operand = Operand.lookupByName(operand_json)
+
+    return CompoundExpression(expressions, operand)
+
+
+def matchExpressionFromJSON(json):
+    try:
+        field_json = json["field"]
+        value_json = json["value"]
+        match_json = json["match"]
+        flags_json = json["flags"]
+    except KeyError as e:
+        raise ValueError(
+            "JSON match expression must have {!r} key.".format(e[0])
+        )
+
+    raise NotImplementedError()
+
+    fieldName = NotImplemented, field_json   # Need service...
+    fieldValue = NotImplemented, value_json  # Need to cast to correct value
+    matchType = MatchType.lookupByName(match_json)
+    flags = NotImplemented, flags_json       # Need to handle composite flags
+
+    MatchFlags  # Shh, flakes
+
+    return MatchExpression(
+        fieldName, fieldValue,
+        matchType=matchType, flags=flags,
+    )
+
+
+def to_json_text(obj):
+    """
+    Convert an object into JSON text.
+
+    @param obj: An object that is serializable to JSON.
+    @type obj: L{object}
+
+    @return: JSON text.
+    @rtype: L{unicode}
+    """
+    return dumps(obj, separators=(',', ':')).decode("UTF-8")

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/dps/server.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/dps/server.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/dps/server.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -15,32 +15,32 @@
 ##
 
 import cPickle as pickle
-import os
 import uuid
 
 from twext.python.log import Logger
-from twext.who.idirectory import RecordType
+from twext.who.expression import MatchType, MatchFlags, Operand
 from twisted.application import service
 from twisted.application.strports import service as strPortsService
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.protocol import Factory
 from twisted.plugin import IPlugin
 from twisted.protocols import amp
-from twisted.python.filepath import FilePath
+from twisted.python.constants import Names, NamedConstant
 from twisted.python.usage import Options, UsageError
 from twistedcaldav.config import config
 from twistedcaldav.stdconfig import DEFAULT_CONFIG, DEFAULT_CONFIG_FILE
 from txdav.dps.commands import (
     RecordWithShortNameCommand, RecordWithUIDCommand, RecordWithGUIDCommand,
     RecordsWithRecordTypeCommand, RecordsWithEmailAddressCommand,
+    RecordsMatchingTokensCommand, RecordsMatchingFieldsCommand,
+    MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
     # UpdateRecordsCommand, RemoveRecordsCommand
 )
-from twext.who.ldap import DirectoryService as LDAPDirectoryService
-from txdav.who.xml import DirectoryService as XMLDirectoryService
+from txdav.who.util import directoryFromConfig
 from zope.interface import implementer
-from twisted.cred.credentials import UsernamePassword
 
+
 log = Logger()
 
 
@@ -63,15 +63,21 @@
 
     def recordToDict(self, record):
         """
-        This to be replaced by something awesome
+        Turn a record in a dictionary of fields which can be reconstituted
+        within the client
         """
         fields = {}
         if record is not None:
             for field, value in record.fields.iteritems():
-                # print("%s: %s" % (field.name, value))
-                valueType = self._directory.fieldName.valueType(field)
-                if valueType is unicode:
+                valueType = record.service.fieldName.valueType(field)
+                # print("%s: %s (%s)" % (field.name, value, valueType))
+                if valueType in (unicode, bool):
                     fields[field.name] = value
+                elif valueType is uuid.UUID:
+                    fields[field.name] = str(value)
+                elif issubclass(valueType, (Names, NamedConstant)):
+                    fields[field.name] = value.name if value else None
+        # print("Server side fields", fields)
         return fields
 
 
@@ -82,7 +88,7 @@
         shortName = shortName.decode("utf-8")
         log.debug("RecordWithShortName: {r} {n}", r=recordType, n=shortName)
         record = (yield self._directory.recordWithShortName(
-            RecordType.lookupByName(recordType), shortName)
+            self._directory.recordType.lookupByName(recordType), shortName)
         )
         fields = self.recordToDict(record)
         response = {
@@ -130,7 +136,7 @@
         recordType = recordType  # as bytes
         log.debug("RecordsWithRecordType: {r}", r=recordType)
         records = (yield self._directory.recordsWithRecordType(
-            RecordType.lookupByName(recordType))
+            self._directory.recordType.lookupByName(recordType))
         )
         fieldsList = []
         for record in records:
@@ -158,6 +164,132 @@
         returnValue(response)
 
 
+    @RecordsMatchingTokensCommand.responder
+    @inlineCallbacks
+    def recordsMatchingTokens(self, tokens, context=None):
+        tokens = [t.decode("utf-8") for t in tokens]
+        log.debug("RecordsMatchingTokens: {t}", t=(", ".join(tokens)))
+        records = yield self._directory.recordsMatchingTokens(
+            tokens, context=context
+        )
+        fieldsList = []
+        for record in records:
+            fieldsList.append(self.recordToDict(record))
+        response = {
+            "fieldsList": pickle.dumps(fieldsList),
+        }
+        log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
+    @RecordsMatchingFieldsCommand.responder
+    @inlineCallbacks
+    def recordsMatchingFields(self, fields, operand="OR", recordType=None):
+        log.debug("RecordsMatchingFields")
+        newFields = []
+        for fieldName, searchTerm, matchFlags, matchType in fields:
+            fieldName = fieldName.decode("utf-8")
+            searchTerm = searchTerm.decode("utf-8")
+            try:
+                field = self._directory.fieldName.lookupByName(fieldName)
+            except ValueError:
+                field = None
+            if field:
+                valueType = self._directory.fieldName.valueType(field)
+                if valueType is uuid.UUID:
+                    searchTerm = uuid.UUID(searchTerm)
+            matchFlags = MatchFlags.lookupByName(matchFlags.decode("utf-8"))
+            matchType = MatchType.lookupByName(matchType.decode("utf-8"))
+            newFields.append((fieldName, searchTerm, matchFlags, matchType))
+        operand = Operand.lookupByName(operand)
+        if recordType:
+            recordType = self._directory.recordType.lookupByName(recordType)
+        records = yield self._directory.recordsMatchingFields(
+            newFields, operand=operand, recordType=recordType
+        )
+        fieldsList = []
+        for record in records:
+            fieldsList.append(self.recordToDict(record))
+        response = {
+            "fieldsList": pickle.dumps(fieldsList),
+        }
+        log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
+    @MembersCommand.responder
+    @inlineCallbacks
+    def members(self, uid):
+        uid = uid.decode("utf-8")
+        log.debug("Members: {u}", u=uid)
+        try:
+            record = (yield self._directory.recordWithUID(uid))
+        except Exception as e:
+            log.error("Failed in members", error=e)
+            record = None
+
+        fieldsList = []
+        if record is not None:
+            for member in (yield record.members()):
+                fieldsList.append(self.recordToDict(member))
+        response = {
+            "fieldsList": pickle.dumps(fieldsList),
+        }
+        log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
+    @SetMembersCommand.responder
+    @inlineCallbacks
+    def setMembers(self, uid, memberUIDs):
+        uid = uid.decode("utf-8")
+        memberUIDs = [m.decode("utf-8") for m in memberUIDs]
+        log.debug("Set Members: {u} -> {m}", u=uid, m=memberUIDs)
+        try:
+            record = (yield self._directory.recordWithUID(uid))
+        except Exception as e:
+            log.error("Failed in setMembers", error=e)
+            record = None
+
+        if record is not None:
+            memberRecords = []
+            for memberUID in memberUIDs:
+                memberRecord = yield self._directory.recordWithUID(memberUID)
+                if memberRecord is not None:
+                    memberRecords.append(memberRecord)
+            yield record.setMembers(memberRecords)
+            success = True
+        else:
+            success = False
+
+        response = {
+            "success": success,
+        }
+        log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
+    @GroupsCommand.responder
+    @inlineCallbacks
+    def groups(self, uid):
+        uid = uid.decode("utf-8")
+        log.debug("Groups: {u}", u=uid)
+        try:
+            record = (yield self._directory.recordWithUID(uid))
+        except Exception as e:
+            log.error("Failed in groups", error=e)
+            record = None
+
+        fieldsList = []
+        for group in (yield record.groups()):
+            fieldsList.append(self.recordToDict(group))
+        response = {
+            "fieldsList": pickle.dumps(fieldsList),
+        }
+        log.debug("Responding with: {response}", response=response)
+        returnValue(response)
+
+
     @VerifyPlaintextPasswordCommand.responder
     @inlineCallbacks
     def verifyPlaintextPassword(self, uid, password):
@@ -333,36 +465,17 @@
         else:
             setproctitle("CalendarServer Directory Proxy Service")
 
-        directoryType = config.DirectoryProxy.DirectoryType
-        args = config.DirectoryProxy.Arguments
-        kwds = config.DirectoryProxy.Keywords
+        try:
+            directory = directoryFromConfig(config)
+        except Exception as e:
+            log.error("Failed to create directory service", error=e)
+            raise
 
-        if directoryType == "OD":
-            from twext.who.opendirectory import DirectoryService as ODDirectoryService
-            directory = ODDirectoryService(*args, **kwds)
+        log.info("Created directory service")
 
-        elif directoryType == "LDAP":
-            authDN = kwds.pop("authDN", "")
-            password = kwds.pop("password", "")
-            if authDN and password:
-                creds = UsernamePassword(authDN, password)
-            else:
-                creds = None
-            kwds["credentials"] = creds
-            debug = kwds.pop("debug", "")
-            directory = LDAPDirectoryService(*args, _debug=debug, **kwds)
-
-        elif directoryType == "XML":
-            path = kwds.pop("path", "")
-            if not path or not os.path.exists(path):
-                log.error("Path not found for XML directory: {p}", p=path)
-            fp = FilePath(path)
-            directory = XMLDirectoryService(fp, *args, **kwds)
-
-        else:
-            log.error("Invalid DirectoryType: {dt}", dt=directoryType)
-
-        desc = "unix:{path}:mode=660".format(
-            path=config.DirectoryProxy.SocketPath
+        return strPortsService(
+            "unix:{path}:mode=660".format(
+                path=config.DirectoryProxy.SocketPath
+            ),
+            DirectoryProxyAMPFactory(directory)
         )
-        return strPortsService(desc, DirectoryProxyAMPFactory(directory))

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -23,6 +23,7 @@
 
   <record type="user">
     <uid>__sagen__</uid>
+    <guid>B3B1158F-0564-4F5B-81E4-A89EA5FF81B0</guid>
     <short-name>sagen</short-name>
     <full-name>Morgen Sagen</full-name>
     <password>negas</password>
@@ -113,4 +114,10 @@
     <member-uid>__alyssa__</member-uid>
   </record>
 
+  <record type="location">
+    <uid>__sanchezoffice__</uid>
+    <short-name>sanchezoffice</short-name>
+    <full-name>Sanchez Office</full-name>
+  </record>
+
 </directory>

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test_client.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test_client.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/dps/test/test_client.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -15,8 +15,12 @@
 ##
 
 import os
+import uuid
 
-from twext.who.idirectory import RecordType
+from twext.who.expression import (
+    Operand, MatchType, MatchFlags, MatchExpression
+)
+from twext.who.idirectory import RecordType, FieldName
 from twisted.cred.credentials import calcResponse, calcHA1, calcHA2
 from twisted.internet.defer import inlineCallbacks, succeed
 from twisted.protocols.amp import AMP
@@ -25,19 +29,48 @@
 from twisted.trial import unittest
 from txdav.dps.client import DirectoryService
 from txdav.dps.server import DirectoryProxyAMPProtocol
+from txdav.who.directory import CalendarDirectoryServiceMixin
+from twistedcaldav.test.util import StoreTestCase
+from twistedcaldav.config import config
 
 
 testMode = "xml"  # "xml" or "od"
 if testMode == "xml":
+    testShortName = u"wsanchez"
+    testUID = u"__wsanchez__"
+    testPassword = u"zehcnasw"
     from txdav.who.xml import DirectoryService as XMLDirectoryService
+
+    # Mix in the calendar-specific service methods
+    class CalendarXMLDirectoryService(
+        XMLDirectoryService,
+        CalendarDirectoryServiceMixin
+    ):
+        pass
+
 elif testMode == "od":
-    odpw = "secret"
+    testShortName = u"becausedigest"
+    testUID = u"381D56CA-3B89-4AA1-942A-D4BFBC4F6F69"
+    testPassword = u"password"
     from twext.who.opendirectory import DirectoryService as OpenDirectoryService
 
+    # Mix in the calendar-specific service methods
+    class CalendarODDirectoryService(
+        OpenDirectoryService,
+        CalendarDirectoryServiceMixin
+    ):
+        pass
 
 
-class DPSClientTest(unittest.TestCase):
 
+
+class DPSClientSingleDirectoryTest(unittest.TestCase):
+    """
+    Tests the client against a single directory service (as opposed to the
+    augmented, aggregated structure you get from directoryFromConfig(), which
+    is tested in the class below)
+    """
+
     def setUp(self):
 
         # The "local" directory service
@@ -46,9 +79,9 @@
         # The "remote" directory service
         if testMode == "xml":
             path = os.path.join(os.path.dirname(__file__), "test.xml")
-            remoteDirectory = XMLDirectoryService(FilePath(path))
+            remoteDirectory = CalendarXMLDirectoryService(FilePath(path))
         elif testMode == "od":
-            remoteDirectory = OpenDirectoryService()
+            remoteDirectory = CalendarODDirectoryService()
 
         # Connect the two services directly via an IOPump
         client = AMP()
@@ -73,64 +106,382 @@
 
     @inlineCallbacks
     def test_uid(self):
-        record = (yield self.directory.recordWithUID("__dre__"))
-        self.assertEquals(record.shortNames, [u"dre"])
+        record = (yield self.directory.recordWithUID(testUID))
+        self.assertTrue(testShortName in record.shortNames)
 
 
     @inlineCallbacks
     def test_shortName(self):
         record = (yield self.directory.recordWithShortName(
             RecordType.user,
-            "wsanchez"
+            testShortName
         ))
-        self.assertEquals(record.shortNames, [u'wsanchez', u'wilfredo_sanchez'])
+        self.assertEquals(record.uid, testUID)
 
 
+    def test_guid(self):
+        if testMode == "od":
+            record = (yield self.directory.recordWithGUID(testUID))
+            self.assertTrue(testShortName in record.shortNames)
+
+
     @inlineCallbacks
-    def test_guid(self):
-        record = (yield self.directory.recordWithGUID(
-            "A3B1158F-0564-4F5B-81E4-A89EA5FF81B0"
+    def test_recordType(self):
+        if testMode != "od":
+            records = (yield self.directory.recordsWithRecordType(
+                RecordType.user
+            ))
+            self.assertEquals(len(records), 9)
+
+
+    @inlineCallbacks
+    def test_emailAddress(self):
+        if testMode == "xml":
+            records = (yield self.directory.recordsWithEmailAddress(
+                u"cdaboo at bitbucket.calendarserver.org"
+            ))
+            self.assertEquals(len(records), 1)
+            self.assertEquals(records[0].shortNames, [u"cdaboo"])
+
+
+    @inlineCallbacks
+    def test_recordsMatchingTokens(self):
+        records = (yield self.directory.recordsMatchingTokens(
+            [u"anche"]
         ))
-        self.assertEquals(record.shortNames, [u'dre'])
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
 
 
     @inlineCallbacks
+    def test_recordsMatchingFields_anyType(self):
+        fields = (
+            (u"fullNames", "anche", MatchFlags.caseInsensitive, MatchType.contains),
+            (u"fullNames", "morgen", MatchFlags.caseInsensitive, MatchType.contains),
+        )
+        records = (yield self.directory.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=None
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("sagen" in matchingShortNames)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+        self.assertTrue("sanchezoffice" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingFields_oneType(self):
+        fields = (
+            (u"fullNames", "anche", MatchFlags.caseInsensitive, MatchType.contains),
+        )
+        records = (yield self.directory.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=RecordType.user
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+        # This location should *not* appear in the results
+        self.assertFalse("sanchezoffice" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingFields_unsupportedField(self):
+        fields = (
+            (u"fullNames", "anche", MatchFlags.caseInsensitive, MatchType.contains),
+            # This should be ignored:
+            (u"foo", "bar", MatchFlags.caseInsensitive, MatchType.contains),
+        )
+        records = (yield self.directory.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=None
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+        self.assertTrue("sanchezoffice" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingFields_nonUnicode(self):
+        fields = (
+            (u"guid", uuid.UUID("A3B1158F-0564-4F5B-81E4-A89EA5FF81B0"),
+                MatchFlags.caseInsensitive, MatchType.equals),
+        )
+        records = (yield self.directory.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=None
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertFalse("wsanchez" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsFromMatchExpression(self):
+        expression = MatchExpression(
+            FieldName.uid,
+            testUID,
+            MatchType.equals,
+            MatchFlags.none
+        )
+        records = yield self.directory.recordsFromExpression(expression)
+        self.assertEquals(len(records), 1)
+
+    test_recordsFromMatchExpression.todo = "Won't work until we can serialize expressions"
+
+
+    @inlineCallbacks
+    def test_verifyPlaintextPassword(self):
+        expectations = (
+            (testPassword, True),  # Correct
+            ("wrong", False)  # Incorrect
+        )
+        record = (
+            yield self.directory.recordWithShortName(
+                RecordType.user,
+                testShortName
+            )
+        )
+
+        for password, answer in expectations:
+            authenticated = (yield record.verifyPlaintextPassword(password))
+            self.assertEquals(authenticated, answer)
+
+
+    @inlineCallbacks
+    def test_verifyHTTPDigest(self):
+        expectations = (
+            (testPassword, True),  # Correct
+            ("wrong", False)  # Incorrect
+        )
+        record = (
+            yield self.directory.recordWithShortName(
+                RecordType.user,
+                testShortName
+            )
+        )
+
+        realm = "host.example.com"
+        nonce = "128446648710842461101646794502"
+        algorithm = "md5"
+        uri = "http://host.example.com"
+        method = "GET"
+
+        for password, answer in expectations:
+            for qop, nc, cnonce in (
+                ("", "", ""),
+                ("auth", "00000001", "/rrD6TqPA3lHRmg+fw/vyU6oWoQgzK7h9yWrsCmv/lE="),
+            ):
+                response = calcResponse(
+                    calcHA1(algorithm, testShortName, realm, password, nonce, cnonce),
+                    calcHA2(algorithm, method, uri, qop, None),
+                    algorithm, nonce, nc, cnonce, qop)
+
+                authenticated = (
+                    yield record.verifyHTTPDigest(
+                        testShortName, realm, uri, nonce, cnonce, algorithm, nc, qop,
+                        response, method
+                    )
+                )
+                self.assertEquals(authenticated, answer)
+
+
+
+
+
+class DPSClientAugmentedAggregateDirectoryTest(StoreTestCase):
+    """
+    Similar to the above tests, but in the context of the directory structure
+    that directoryFromConfig() returns
+    """
+
+    wsanchezUID = u"6423F94A-6B76-4A3A-815B-D52CFD77935D"
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(DPSClientAugmentedAggregateDirectoryTest, self).setUp()
+
+        # The "local" directory service
+        self.client = DirectoryService(None)
+
+        # The "remote" directory service
+        remoteDirectory = self.directory
+
+        # Connect the two services directly via an IOPump
+        client = AMP()
+        server = DirectoryProxyAMPProtocol(remoteDirectory)
+        pump = returnConnected(server, client)
+
+        # Replace the normal _getConnection method with one that bypasses any
+        # actual networking
+        self.patch(self.client, "_getConnection", lambda: succeed(client))
+
+        # Wrap the normal _call method with one that flushes the IOPump
+        # afterwards
+        origCall = self.client._call
+
+        def newCall(*args, **kwds):
+            d = origCall(*args, **kwds)
+            pump.flush()
+            return d
+
+        self.patch(self.client, "_call", newCall)
+
+
+    def configure(self):
+        """
+        Override configuration hook to turn on wiki.
+        """
+        super(DPSClientAugmentedAggregateDirectoryTest, self).configure()
+        self.patch(config.Authentication.Wiki, "Enabled", True)
+
+
+    @inlineCallbacks
+    def test_uid(self):
+        record = (yield self.client.recordWithUID(self.wsanchezUID))
+        self.assertTrue(u"wsanchez" in record.shortNames)
+
+
+    @inlineCallbacks
+    def test_shortName(self):
+        record = (yield self.client.recordWithShortName(
+            RecordType.user,
+            u"wsanchez"
+        ))
+        self.assertEquals(record.uid, self.wsanchezUID)
+
+
+    def test_guid(self):
+        record = yield self.client.recordWithGUID(self.wsanchezUID)
+        self.assertTrue(u"wsanchez" in record.shortNames)
+
+
+    @inlineCallbacks
     def test_recordType(self):
-        records = (yield self.directory.recordsWithRecordType(
+        records = (yield self.client.recordsWithRecordType(
             RecordType.user
         ))
-        self.assertEquals(len(records), 9)
+        self.assertEquals(len(records), 35)
 
 
     @inlineCallbacks
     def test_emailAddress(self):
-        records = (yield self.directory.recordsWithEmailAddress(
-            "cdaboo at bitbucket.calendarserver.org"
+        records = (yield self.client.recordsWithEmailAddress(
+            u"wsanchez at example.com"
         ))
         self.assertEquals(len(records), 1)
-        self.assertEquals(records[0].shortNames, [u"cdaboo"])
+        self.assertEquals(records[0].shortNames, [u"wsanchez"])
 
 
     @inlineCallbacks
+    def test_recordsMatchingTokens(self):
+        records = (yield self.client.recordsMatchingTokens(
+            [u"anche"]
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingFields_anyType(self):
+        fields = (
+            (u"fullNames", "anche", MatchFlags.caseInsensitive, MatchType.contains),
+            (u"fullNames", "morgen", MatchFlags.caseInsensitive, MatchType.contains),
+        )
+        records = (yield self.client.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=None
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("sagen" in matchingShortNames)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+        self.assertTrue("sanchezoffice" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingFields_oneType(self):
+        fields = (
+            (u"fullNames", "anche", MatchFlags.caseInsensitive, MatchType.contains),
+        )
+        records = (yield self.client.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=RecordType.user
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+        # This location should *not* appear in the results
+        self.assertFalse("sanchezoffice" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsMatchingFields_unsupportedField(self):
+        fields = (
+            (u"fullNames", "anche", MatchFlags.caseInsensitive, MatchType.contains),
+            # This should be ignored:
+            (u"foo", "bar", MatchFlags.caseInsensitive, MatchType.contains),
+        )
+        records = (yield self.client.recordsMatchingFields(
+            fields, operand=Operand.OR, recordType=None
+        ))
+        matchingShortNames = set()
+        for r in records:
+            for shortName in r.shortNames:
+                matchingShortNames.add(shortName)
+        self.assertTrue("dre" in matchingShortNames)
+        self.assertTrue("wsanchez" in matchingShortNames)
+        self.assertTrue("sanchezoffice" in matchingShortNames)
+
+
+    @inlineCallbacks
+    def test_recordsFromMatchExpression(self):
+        expression = MatchExpression(
+            FieldName.uid,
+            u"wsanchez",
+            MatchType.equals,
+            MatchFlags.none
+        )
+        records = yield self.client.recordsFromExpression(expression)
+        self.assertEquals(len(records), 1)
+
+    test_recordsFromMatchExpression.todo = "Won't work until we can serialize expressions"
+
+
+    @inlineCallbacks
     def test_verifyPlaintextPassword(self):
-        if testMode == "xml":
-            expectations = (
-                ("erd", True),    # Correct
-                ("wrong", False)  # Incorrect
+        expectations = (
+            (u"zehcnasw", True),  # Correct
+            ("wrong", False)  # Incorrect
+        )
+        record = (
+            yield self.client.recordWithShortName(
+                RecordType.user,
+                u"wsanchez"
             )
-            record = (
-                yield self.directory.recordWithShortName(RecordType.user, "dre")
-            )
-        elif testMode == "od":
-            expectations = (
-                (odpw, True),     # Correct
-                ("wrong", False)  # Incorrect
-            )
-            record = (
-                yield self.directory.recordWithGUID(
-                    "D0B38B00-4166-11DD-B22C-A07C87F02F6A"
-                )
-            )
+        )
 
         for password, answer in expectations:
             authenticated = (yield record.verifyPlaintextPassword(password))
@@ -139,26 +490,16 @@
 
     @inlineCallbacks
     def test_verifyHTTPDigest(self):
-        if testMode == "xml":
-            username = "dre"
-            expectations = (
-                ("erd", True),    # Correct
-                ("wrong", False)  # Incorrect
+        expectations = (
+            (u"zehcnasw", True),  # Correct
+            ("wrong", False)  # Incorrect
+        )
+        record = (
+            yield self.client.recordWithShortName(
+                RecordType.user,
+                u"wsanchez"
             )
-            record = (
-                yield self.directory.recordWithShortName(RecordType.user, "dre")
-            )
-        elif testMode == "od":
-            username = "sagen"
-            expectations = (
-                (odpw, True),     # Correct
-                ("wrong", False)  # Incorrect
-            )
-            record = (
-                yield self.directory.recordWithGUID(
-                    "D0B38B00-4166-11DD-B22C-A07C87F02F6A"
-                )
-            )
+        )
 
         realm = "host.example.com"
         nonce = "128446648710842461101646794502"
@@ -172,13 +513,13 @@
                 ("auth", "00000001", "/rrD6TqPA3lHRmg+fw/vyU6oWoQgzK7h9yWrsCmv/lE="),
             ):
                 response = calcResponse(
-                    calcHA1(algorithm, username, realm, password, nonce, cnonce),
+                    calcHA1(algorithm, u"wsanchez", realm, password, nonce, cnonce),
                     calcHA2(algorithm, method, uri, qop, None),
                     algorithm, nonce, nc, cnonce, qop)
 
                 authenticated = (
                     yield record.verifyHTTPDigest(
-                        username, realm, uri, nonce, cnonce, algorithm, nc, qop,
+                        u"wsanchez", realm, uri, nonce, cnonce, algorithm, nc, qop,
                         response, method
                     )
                 )

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/augment.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/augment.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/augment.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/augment.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,406 @@
+# -*- test-case-name: txdav.who.test.test_augment -*-
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+Augmenting Directory Service
+"""
+
+__all__ = [
+    "AugmentedDirectoryService",
+]
+
+from zope.interface import implementer
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.directory.augment import AugmentRecord
+from twext.python.log import Logger
+from twext.who.directory import DirectoryRecord
+from twext.who.directory import DirectoryService as BaseDirectoryService
+from twext.who.idirectory import (
+    IDirectoryService, RecordType, FieldName as BaseFieldName
+)
+from twext.who.util import ConstantsContainer
+
+from txdav.common.idirectoryservice import IStoreDirectoryService
+from txdav.who.directory import (
+    CalendarDirectoryRecordMixin, CalendarDirectoryServiceMixin,
+)
+from txdav.who.idirectory import (
+    AutoScheduleMode, FieldName, RecordType as CalRecordType
+)
+
+log = Logger()
+
+
+
+ at implementer(IDirectoryService, IStoreDirectoryService)
+class AugmentedDirectoryService(
+    BaseDirectoryService, CalendarDirectoryServiceMixin
+):
+    """
+    Augmented directory service.
+
+    This is a directory service that wraps an L{IDirectoryService} and augments
+    directory records with additional or modified fields.
+    """
+
+    fieldName = ConstantsContainer((
+        BaseFieldName,
+        FieldName,
+    ))
+
+
+    def __init__(self, directory, store, augmentDB):
+        BaseDirectoryService.__init__(self, directory.realmName)
+        self._directory = directory
+        self._store = store
+        self._augmentDB = augmentDB
+
+
+    @property
+    def recordType(self):
+        # Defer to the directory service we're augmenting
+        return self._directory.recordType
+
+
+    def recordTypes(self):
+        # Defer to the directory service we're augmenting
+        return self._directory.recordTypes()
+
+
+    @inlineCallbacks
+    def recordsFromExpression(self, expression):
+        records = yield self._directory.recordsFromExpression(expression)
+        augmented = []
+        for record in records:
+            record = yield self._augment(record)
+            augmented.append(record)
+        returnValue(augmented)
+
+
+    @inlineCallbacks
+    def recordsWithFieldValue(self, fieldName, value):
+        records = yield self._directory.recordsWithFieldValue(
+            fieldName, value
+        )
+        augmented = []
+        for record in records:
+            record = yield self._augment(record)
+            augmented.append(record)
+        returnValue(augmented)
+
+
+    @inlineCallbacks
+    def recordWithUID(self, uid):
+        # MOVE2WHO, REMOVE THIS:
+        if not isinstance(uid, unicode):
+            # log.warn("Need to change uid to unicode")
+            uid = uid.decode("utf-8")
+
+        record = yield self._directory.recordWithUID(uid)
+        record = yield self._augment(record)
+        returnValue(record)
+
+
+    @inlineCallbacks
+    def recordWithGUID(self, guid):
+        record = yield self._directory.recordWithGUID(guid)
+        record = yield self._augment(record)
+        returnValue(record)
+
+
+    @inlineCallbacks
+    def recordsWithRecordType(self, recordType):
+        records = yield self._directory.recordsWithRecordType(recordType)
+        augmented = []
+        for record in records:
+            record = yield self._augment(record)
+            augmented.append(record)
+        returnValue(augmented)
+
+
+    @inlineCallbacks
+    def recordWithShortName(self, recordType, shortName):
+        # MOVE2WHO, REMOVE THIS:
+        if not isinstance(shortName, unicode):
+            # log.warn("Need to change shortName to unicode")
+            shortName = shortName.decode("utf-8")
+
+        record = yield self._directory.recordWithShortName(
+            recordType, shortName
+        )
+        record = yield self._augment(record)
+        returnValue(record)
+
+
+    @inlineCallbacks
+    def recordsWithEmailAddress(self, emailAddress):
+        # MOVE2WHO, REMOVE THIS:
+        if not isinstance(emailAddress, unicode):
+            # log.warn("Need to change emailAddress to unicode")
+            emailAddress = emailAddress.decode("utf-8")
+
+        records = yield self._directory.recordsWithEmailAddress(emailAddress)
+        augmented = []
+        for record in records:
+            record = yield self._augment(record)
+            augmented.append(record)
+        returnValue(augmented)
+
+
+    @inlineCallbacks
+    def updateRecords(self, records, create=False):
+        """
+        Pull out the augmented fields from each record, apply those to the
+        augments database, then update the base records.
+        """
+
+        baseRecords = []
+        augmentRecords = []
+
+        for record in records:
+
+            # Split out the base fields from the augment fields
+            baseFields, augmentFields = self._splitFields(record)
+
+            if augmentFields:
+                # Create an AugmentRecord
+                autoScheduleMode = {
+                    AutoScheduleMode.none: "none",
+                    AutoScheduleMode.accept: "accept-always",
+                    AutoScheduleMode.decline: "decline-always",
+                    AutoScheduleMode.acceptIfFree: "accept-if-free",
+                    AutoScheduleMode.declineIfBusy: "decline-if-busy",
+                    AutoScheduleMode.acceptIfFreeDeclineIfBusy: "automatic",
+                }.get(augmentFields.get(FieldName.autoScheduleMode, None), None)
+
+                kwargs = {
+                    "uid": record.uid,
+                    "autoScheduleMode": autoScheduleMode,
+                }
+                if FieldName.hasCalendars in augmentFields:
+                    kwargs["enabledForCalendaring"] = augmentFields[FieldName.hasCalendars]
+                if FieldName.hasContacts in augmentFields:
+                    kwargs["enabledForAddressBooks"] = augmentFields[FieldName.hasContacts]
+                if FieldName.loginAllowed in augmentFields:
+                    kwargs["enabledForLogin"] = augmentFields[FieldName.loginAllowed]
+                if FieldName.autoAcceptGroup in augmentFields:
+                    kwargs["autoAcceptGroup"] = augmentFields[FieldName.autoAcceptGroup]
+                if FieldName.serviceNodeUID in augmentFields:
+                    kwargs["serverID"] = augmentFields[FieldName.serviceNodeUID]
+                augmentRecord = AugmentRecord(**kwargs)
+
+                augmentRecords.append(augmentRecord)
+
+            # Create new base records:
+            baseRecords.append(DirectoryRecord(self._directory, baseFields))
+
+        # Apply the augment records
+        if augmentRecords:
+            yield self._augmentDB.addAugmentRecords(augmentRecords)
+
+        # Apply the base records
+        if baseRecords:
+            yield self._directory.updateRecords(baseRecords, create=create)
+
+
+    def _splitFields(self, record):
+        """
+        Returns a tuple of two dictionaries; the first contains all the non
+        augment fields, and the second contains all the augment fields.
+        """
+        if record is None:
+            return None
+
+        augmentFields = {}
+        baseFields = record.fields.copy()
+        for field in (
+            FieldName.loginAllowed,
+            FieldName.hasCalendars, FieldName.hasContacts,
+            FieldName.autoScheduleMode, FieldName.autoAcceptGroup,
+            FieldName.serviceNodeUID
+        ):
+            if field in baseFields:
+                augmentFields[field] = baseFields[field]
+                del baseFields[field]
+
+        return (baseFields, augmentFields)
+
+
+    def removeRecords(self, uids):
+        self._augmentDB.removeAugmentRecords(uids)
+        return self._directory.removeRecords(uids)
+
+
+    def _assignToField(self, fields, name, value):
+        field = self.fieldName.lookupByName(name)
+        fields[field] = value
+
+
+
+    @inlineCallbacks
+    def _augment(self, record):
+        if record is None:
+            returnValue(None)
+
+        try:
+            augmentRecord = yield self._augmentDB.getAugmentRecord(
+                record.uid,
+                self.recordTypeToOldName(record.recordType)
+            )
+        except KeyError:
+            # Augments does not know about this record type, so return
+            # the original record
+            returnValue(record)
+
+        fields = record.fields.copy()
+
+        # print("Got augment record", augmentRecord)
+
+        if augmentRecord:
+
+            self._assignToField(
+                fields, "hasCalendars",
+                augmentRecord.enabledForCalendaring
+            )
+
+            self._assignToField(
+                fields, "hasContacts",
+                augmentRecord.enabledForAddressBooks
+            )
+
+            autoScheduleMode = {
+                "none": AutoScheduleMode.none,
+                "accept-always": AutoScheduleMode.accept,
+                "decline-always": AutoScheduleMode.decline,
+                "accept-if-free": AutoScheduleMode.acceptIfFree,
+                "decline-if-busy": AutoScheduleMode.declineIfBusy,
+                "automatic": AutoScheduleMode.acceptIfFreeDeclineIfBusy,
+            }.get(augmentRecord.autoScheduleMode, None)
+
+            # Resources/Locations default to automatic
+            if record.recordType in (
+                CalRecordType.location,
+                CalRecordType.resource
+            ):
+                if autoScheduleMode is None:
+                    autoScheduleMode = AutoScheduleMode.acceptIfFreeDeclineIfBusy
+
+            self._assignToField(
+                fields, "autoScheduleMode",
+                autoScheduleMode
+            )
+
+            if augmentRecord.autoAcceptGroup is not None:
+                self._assignToField(
+                    fields, "autoAcceptGroup",
+                    augmentRecord.autoAcceptGroup.decode("utf-8")
+                )
+
+            self._assignToField(
+                fields, "loginAllowed",
+                augmentRecord.enabledForLogin
+            )
+
+            self._assignToField(
+                fields, "serviceNodeUID",
+                augmentRecord.serverID.decode("utf-8")
+            )
+
+            if (
+                (
+                    fields.get(
+                        self.fieldName.lookupByName("hasCalendars"), False
+                    ) or
+                    fields.get(
+                        self.fieldName.lookupByName("hasContacts"), False
+                    )
+                ) and
+                record.recordType == RecordType.group
+            ):
+                self._assignToField(fields, "hasCalendars", False)
+                self._assignToField(fields, "hasContacts", False)
+
+                # For augment records cloned from the Default augment record,
+                # don't emit this message:
+                if not augmentRecord.clonedFromDefault:
+                    log.error(
+                        "Group {record} cannot be enabled for "
+                        "calendaring or address books",
+                        record=record
+                    )
+
+        else:
+            self._assignToField(fields, "hasCalendars", False)
+            self._assignToField(fields, "hasContacts", False)
+            self._assignToField(fields, "loginAllowed", False)
+
+        # print("Augmented fields", fields)
+
+        # Clone to a new record with the augmented fields
+        returnValue(AugmentedDirectoryRecord(self, record, fields))
+
+
+
+class AugmentedDirectoryRecord(DirectoryRecord, CalendarDirectoryRecordMixin):
+    """
+    Augmented directory record.
+    """
+
+    def __init__(self, service, baseRecord, augmentedFields):
+        DirectoryRecord.__init__(self, service, augmentedFields)
+        self._baseRecord = baseRecord
+
+
+    @inlineCallbacks
+    def members(self):
+        augmented = []
+        records = yield self._baseRecord.members()
+
+        for record in records:
+            augmented.append((yield self.service._augment(record)))
+
+        returnValue(augmented)
+
+
+    @inlineCallbacks
+    def groups(self):
+        augmented = []
+
+        txn = self.service._store.newTransaction()
+        groupUIDs = yield txn.groupsFor(self.uid)
+
+        for groupUID in groupUIDs:
+            groupRecord = yield self.service.recordWithUID(
+                groupUID
+            )
+            if groupRecord:
+                augmented.append((yield self.service._augment(groupRecord)))
+
+        returnValue(augmented)
+
+
+    def verifyPlaintextPassword(self, password):
+        return self._baseRecord.verifyPlaintextPassword(password)
+
+
+    def verifyHTTPDigest(self, *args):
+        return self._baseRecord.verifyHTTPDigest(*args)
+
+
+    def accessForRecord(self, record):
+        return self._baseRecord.accessForRecord(record)

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/delegates.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/delegates.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/delegates.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,4 +1,4 @@
-# -*- test-case-name: twext.who.test.test_delegates -*-
+# -*- test-case-name: txdav.who.test.test_delegates -*-
 ##
 # Copyright (c) 2013 Apple Inc. All rights reserved.
 #
@@ -19,13 +19,202 @@
 Delegate assignments
 """
 
-from twisted.internet.defer import inlineCallbacks, returnValue
-from twext.who.idirectory import RecordType
+from twisted.python.constants import Names, NamedConstant
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twext.python.log import Logger
+from twext.who.idirectory import (
+    RecordType as BaseRecordType, FieldName, NotAllowedError
+)
+from twext.who.directory import (
+    DirectoryService as BaseDirectoryService,
+    DirectoryRecord as BaseDirectoryRecord
+)
+from twext.who.expression import MatchExpression, MatchType
+
 log = Logger()
 
 
+
+class RecordType(Names):
+    """
+    Constants for read-only delegates and read-write delegate groups
+    """
+
+    readDelegateGroup = NamedConstant()
+    readDelegateGroup.description = u"read-delegate-group"
+
+    writeDelegateGroup = NamedConstant()
+    writeDelegateGroup.description = u"write-delegate-group"
+
+    readDelegatorGroup = NamedConstant()
+    readDelegatorGroup.description = u"read-delegator-group"
+
+    writeDelegatorGroup = NamedConstant()
+    writeDelegatorGroup.description = u"write-delegator-group"
+
+
+
+class DirectoryRecord(BaseDirectoryRecord):
+
+    @inlineCallbacks
+    def members(self, expanded=False):
+        """
+        If this is a readDelegateGroup or writeDelegateGroup, the members
+        will consist of the records who are delegates *of* this record.
+        If this is a readDelegatorGroup or writeDelegatorGroup,
+        the members will consist of the records who have delegated *to*
+        this record.
+        """
+        parentUID, proxyType = self.uid.split(u"#")
+
+        txn = self.service._store.newTransaction()
+
+        if self.recordType in (
+            RecordType.readDelegateGroup, RecordType.writeDelegateGroup
+        ):  # Members are delegates of this record
+            readWrite = (self.recordType is RecordType.writeDelegateGroup)
+            delegateUIDs = (
+                yield txn.delegates(parentUID, readWrite, expanded=expanded)
+            )
+
+        else:  # Members have delegated to this record
+            readWrite = (self.recordType is RecordType.writeDelegatorGroup)
+            delegateUIDs = (
+                yield txn.delegators(parentUID, readWrite)
+            )
+
+        records = []
+        for uid in delegateUIDs:
+            if uid != parentUID:
+                record = yield self.service._masterDirectory.recordWithUID(uid)
+                if record is not None:
+                    records.append(record)
+        yield txn.commit()
+
+        returnValue(records)
+
+
+
+    @inlineCallbacks
+    def setMembers(self, memberRecords):
+        """
+        Replace the members of this group with the new members.
+
+        @param memberRecords: The new members of the group
+        @type memberRecords: iterable of L{iDirectoryRecord}s
+        """
+        if self.recordType not in (
+            RecordType.readDelegateGroup, RecordType.writeDelegateGroup
+        ):
+            raise NotAllowedError("Setting members not supported")
+
+        parentUID, proxyType = self.uid.split(u"#")
+        readWrite = (self.recordType is RecordType.writeDelegateGroup)
+
+        log.debug(
+            "Setting delegate assignments for {u} ({rw}) to {m}",
+            u=parentUID, rw=("write" if readWrite else "read"),
+            m=[r.uid for r in memberRecords]
+        )
+
+        txn = self.service._store.newTransaction()
+
+        yield txn.removeDelegates(parentUID, readWrite)
+        yield txn.removeDelegateGroups(parentUID, readWrite)
+
+        delegator = (
+            yield self.service._masterDirectory.recordWithUID(parentUID)
+        )
+
+        for delegate in memberRecords:
+            yield addDelegate(txn, delegator, delegate, readWrite)
+
+        yield txn.commit()
+
+
+
+def recordTypeToProxyType(recordType):
+    return {
+        RecordType.readDelegateGroup: "calendar-proxy-read",
+        RecordType.writeDelegateGroup: "calendar-proxy-write",
+        RecordType.readDelegatorGroup: "calendar-proxy-read-for",
+        RecordType.writeDelegatorGroup: "calendar-proxy-write-for",
+    }.get(recordType, None)
+
+
+def proxyTypeToRecordType(proxyType):
+    return {
+        "calendar-proxy-read": RecordType.readDelegateGroup,
+        "calendar-proxy-write": RecordType.writeDelegateGroup,
+        "calendar-proxy-read-for": RecordType.readDelegatorGroup,
+        "calendar-proxy-write-for": RecordType.writeDelegatorGroup,
+    }.get(proxyType, None)
+
+
+
+class DirectoryService(BaseDirectoryService):
+    """
+    Delegate directory service
+    """
+
+    recordType = RecordType
+
+
+    def __init__(self, realmName, store):
+        BaseDirectoryService.__init__(self, realmName)
+        self._store = store
+        self._masterDirectory = None
+
+
+    def setMasterDirectory(self, masterDirectory):
+        self._masterDirectory = masterDirectory
+
+
+    def recordWithShortName(self, recordType, shortName):
+        uid = shortName + "#" + recordTypeToProxyType(recordType)
+
+        record = DirectoryRecord(self, {
+            FieldName.uid: uid,
+            FieldName.recordType: recordType,
+            FieldName.shortNames: (uid,),
+        })
+        return succeed(record)
+
+
+    def recordWithUID(self, uid):
+        if "#" not in uid:  # Not a delegate group uid
+            return succeed(None)
+        uid, proxyType = uid.split("#")
+        recordType = proxyTypeToRecordType(proxyType)
+        if recordType is None:
+            return succeed(None)
+        return self.recordWithShortName(recordType, uid)
+
+
+    @inlineCallbacks
+    def recordsFromExpression(self, expression, records=None):
+        """
+        It's only ever appropriate to look up delegate group record by
+        shortName or uid.  When wrapped by an aggregate directory, looking up
+        by shortName will already go directly to recordWithShortName.  However
+        when looking up by UID, it won't.  Inspect the expression to see if
+        it's one we can handle.
+        """
+        if isinstance(expression, MatchExpression):
+            if(
+                (expression.fieldName is FieldName.uid) and
+                (expression.matchType is MatchType.equals) and
+                ("#" in expression.fieldValue)
+            ):
+                record = yield self.recordWithUID(expression.fieldValue)
+                if record is not None:
+                    returnValue((record,))
+
+        returnValue(())
+
+
+
 @inlineCallbacks
 def addDelegate(txn, delegator, delegate, readWrite):
     """
@@ -39,12 +228,12 @@
     @param readWrite: if True, read and write access is granted; read-only
         access otherwise
     """
-    if delegate.recordType == RecordType.group:
+    if delegate.recordType == BaseRecordType.group:
         # find the groupID
-        groupID, name, membershipHash = (yield txn.groupByGUID(delegate.guid))
-        yield txn.addDelegateGroup(delegator.guid, groupID, readWrite)
+        groupID, name, membershipHash = (yield txn.groupByUID(delegate.uid))
+        yield txn.addDelegateGroup(delegator.uid, groupID, readWrite)
     else:
-        yield txn.addDelegate(delegator.guid, delegate.guid, readWrite)
+        yield txn.addDelegate(delegator.uid, delegate.uid, readWrite)
 
 
 @inlineCallbacks
@@ -60,16 +249,16 @@
     @param readWrite: if True, read and write access is revoked; read-only
         access otherwise
     """
-    if delegate.recordType == RecordType.group:
+    if delegate.recordType == BaseRecordType.group:
         # find the groupID
-        groupID, name, membershipHash = (yield txn.groupByGUID(delegate.guid))
-        yield txn.removeDelegateGroup(delegator.guid, groupID, readWrite)
+        groupID, name, membershipHash = (yield txn.groupByUID(delegate.uid))
+        yield txn.removeDelegateGroup(delegator.uid, groupID, readWrite)
     else:
-        yield txn.removeDelegate(delegator.guid, delegate.guid, readWrite)
+        yield txn.removeDelegate(delegator.uid, delegate.uid, readWrite)
 
 
 @inlineCallbacks
-def delegatesOf(txn, delegator, readWrite):
+def delegatesOf(txn, delegator, readWrite, expanded=False):
     """
     Return the records of the delegates of "delegator".  The type of access
     is specified by the "readWrite" parameter.
@@ -83,10 +272,12 @@
     """
     records = []
     directory = delegator.service
-    delegateGUIDs = (yield txn.delegates(delegator.guid, readWrite))
-    for guid in delegateGUIDs:
-        if guid != delegator.guid:
-            record = (yield directory.recordWithGUID(guid))
+    delegateUIDs = (
+        yield txn.delegates(delegator.uid, readWrite, expanded=expanded)
+    )
+    for uid in delegateUIDs:
+        if uid != delegator.uid:
+            record = (yield directory.recordWithUID(uid))
             if record is not None:
                 records.append(record)
     returnValue(records)
@@ -107,18 +298,10 @@
     """
     records = []
     directory = delegate.service
-    delegatorGUIDs = (yield txn.delegators(delegate.guid, readWrite))
-    for guid in delegatorGUIDs:
-        if guid != delegate.guid:
-            record = (yield directory.recordWithGUID(guid))
+    delegatorUIDs = (yield txn.delegators(delegate.uid, readWrite))
+    for uid in delegatorUIDs:
+        if uid != delegate.uid:
+            record = (yield directory.recordWithUID(uid))
             if record is not None:
                 records.append(record)
     returnValue(records)
-
-
-def allGroupDelegates(txn):
-    """
-    @return: the GUIDs of all groups which are currently delegated to
-    @rtype: a Deferred which fires with a set() of GUID strings
-    """
-    return txn.allGroupDelegates()

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/directory.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/directory.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/directory.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/directory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,498 @@
+##
+# Copyright (c) 2006-2014 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.
+##
+
+"""
+Calendar/Contacts specific methods for DirectoryRecord
+"""
+
+import uuid
+
+from twext.python.log import Logger
+from twext.who.expression import (
+    MatchType, Operand, MatchExpression, CompoundExpression, MatchFlags
+)
+from twext.who.idirectory import RecordType as BaseRecordType
+from twisted.cred.credentials import UsernamePassword
+from twisted.internet.defer import inlineCallbacks, returnValue
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
+from txdav.who.delegates import RecordType as DelegateRecordType
+from txdav.who.idirectory import (
+    RecordType as DAVRecordType, AutoScheduleMode
+)
+from txweb2.auth.digest import DigestedCredentials
+
+
+log = Logger()
+
+
+__all__ = [
+    "CalendarDirectoryRecordMixin",
+    "CalendarDirectoryServiceMixin",
+]
+
+
+
+class CalendarDirectoryServiceMixin(object):
+
+    guid = "1332A615-4D3A-41FE-B636-FBE25BFB982E"
+
+    # Must maintain the hack for a bit longer:
+    def setPrincipalCollection(self, principalCollection):
+        """
+        Set the principal service that the directory relies on for doing proxy tests.
+
+        @param principalService: the principal service.
+        @type principalService: L{DirectoryProvisioningResource}
+        """
+        self.principalCollection = principalCollection
+
+
+    @inlineCallbacks
+    def recordWithCalendarUserAddress(self, address):
+        # FIXME: moved this here to avoid circular import problems
+        from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
+        address = normalizeCUAddr(address)
+        record = None
+        if address.startswith("urn:uuid:"):
+            try:
+                guid = uuid.UUID(address[9:])
+            except ValueError:
+                log.info("Invalid GUID: {guid}", guid=address[9:])
+                returnValue(None)
+            record = yield self.recordWithGUID(guid)
+        elif address.startswith("mailto:"):
+            records = yield self.recordsWithEmailAddress(address[7:])
+            if records:
+                returnValue(records[0])
+            else:
+                returnValue(None)
+        elif address.startswith("/principals/"):
+            parts = address.split("/")
+            if len(parts) == 4:
+                if parts[2] == "__uids__":
+                    uid = parts[3]
+                    record = yield self.recordWithUID(uid)
+                else:
+                    # recordType = self.fieldName.lookupByName(parts[2])
+                    recordType = self.oldNameToRecordType(parts[2])
+                    record = yield self.recordWithShortName(recordType, parts[3])
+
+        returnValue(record if record and record.hasCalendars else None)
+
+
+    def recordsMatchingTokens(self, tokens, context=None, limitResults=50,
+                              timeoutSeconds=10):
+        fields = [
+            ("fullNames", MatchType.contains),
+            ("emailAddresses", MatchType.startsWith),
+        ]
+        outer = []
+        for token in tokens:
+            inner = []
+            for name, matchType in fields:
+                inner.append(
+                    MatchExpression(
+                        self.fieldName.lookupByName(name),
+                        token,
+                        matchType,
+                        MatchFlags.caseInsensitive
+                    )
+                )
+            outer.append(
+                CompoundExpression(
+                    inner,
+                    Operand.OR
+                )
+            )
+        expression = CompoundExpression(outer, Operand.AND)
+        return self.recordsFromExpression(expression)
+
+
+    def recordsMatchingFieldsWithCUType(self, fields, operand=Operand.OR,
+                                        cuType=None):
+        if cuType:
+            recordType = CalendarDirectoryRecordMixin.fromCUType(cuType)
+        else:
+            recordType = None
+
+        return self.recordsMatchingFields(
+            fields, operand=operand, recordType=recordType
+        )
+
+
+    def recordsMatchingFields(self, fields, operand=Operand.OR, recordType=None):
+        """
+        @param fields: a iterable of tuples, each tuple consisting of:
+            directory field name (C{unicode})
+            search term (C{unicode})
+            match flags (L{twext.who.expression.MatchFlags})
+            match type (L{twext.who.expression.MatchType})
+        """
+        subExpressions = []
+        for fieldName, searchTerm, matchFlags, matchType in fields:
+            try:
+                field = self.fieldName.lookupByName(fieldName)
+            except ValueError:
+                log.debug(
+                    "Unsupported field name: {fieldName}",
+                    fieldName=fieldName
+                )
+                continue
+            subExpression = MatchExpression(
+                field,
+                searchTerm,
+                matchType,
+                matchFlags
+            )
+            subExpressions.append(subExpression)
+
+        expression = CompoundExpression(subExpressions, operand)
+
+        # AND in the recordType if passed in
+        if recordType is not None:
+            typeExpression = MatchExpression(
+                self.fieldName.recordType,
+                recordType,
+                MatchType.equals,
+                MatchFlags.none
+            )
+            expression = CompoundExpression(
+                [
+                    expression,
+                    typeExpression
+                ],
+                Operand.AND
+            )
+        return self.recordsFromExpression(expression)
+
+
+    _oldRecordTypeNames = {
+        "address": "addresses",
+        "group": "groups",
+        "location": "locations",
+        "resource": "resources",
+        "user": "users",
+        "macOSXServerWiki": "wikis",
+        "readDelegateGroup": "readDelegateGroups",
+        "writeDelegateGroup": "writeDelegateGroups",
+        "readDelegatorGroup": "readDelegatorGroups",
+        "writeDelegatorGroup": "writeDelegatorGroups",
+    }
+
+
+    # Maps record types <--> url path segments, i.e. the segment after
+    # /principals/ e.g. "users" or "groups"
+
+    def recordTypeToOldName(self, recordType):
+        return self._oldRecordTypeNames[recordType.name]
+
+    def oldNameToRecordType(self, oldName):
+        for name, value in self._oldRecordTypeNames.iteritems():
+            if oldName == value:
+                return self.recordType.lookupByName(name)
+        return None
+
+
+
+class CalendarDirectoryRecordMixin(object):
+    """
+    Calendar (and Contacts) specific logic for directory records lives in this
+    class
+    """
+
+
+    @inlineCallbacks
+    def verifyCredentials(self, credentials):
+
+        if isinstance(credentials, UsernamePassword):
+            log.debug("UsernamePassword")
+            returnValue(
+                (yield self.verifyPlaintextPassword(credentials.password))
+            )
+
+        elif isinstance(credentials, DigestedCredentials):
+            log.debug("DigestedCredentials")
+            returnValue(
+                (yield self.verifyHTTPDigest(
+                    self.shortNames[0],
+                    self.service.realmName,
+                    credentials.fields["uri"],
+                    credentials.fields["nonce"],
+                    credentials.fields.get("cnonce", ""),
+                    credentials.fields["algorithm"],
+                    credentials.fields.get("nc", ""),
+                    credentials.fields.get("qop", ""),
+                    credentials.fields["response"],
+                    credentials.method
+                ))
+            )
+
+
+    @property
+    def calendarUserAddresses(self):
+        try:
+            if not self.hasCalendars:
+                return frozenset()
+        except AttributeError:
+            pass
+
+        try:
+            cuas = set(
+                ["mailto:%s" % (emailAddress,)
+                 for emailAddress in self.emailAddresses]
+            )
+        except AttributeError:
+            cuas = set()
+
+        try:
+            if self.guid:
+                if isinstance(self.guid, uuid.UUID):
+                    guid = unicode(self.guid).upper()
+                else:
+                    guid = self.guid
+                cuas.add("urn:uuid:{guid}".format(guid=guid))
+        except AttributeError:
+            # No guid
+            pass
+        cuas.add(u"/principals/__uids__/{uid}/".format(uid=self.uid))
+        for shortName in self.shortNames:
+            cuas.add(u"/principals/{rt}/{sn}/".format(
+                rt=self.service.recordTypeToOldName(self.recordType),
+                sn=shortName)
+            )
+        return frozenset(cuas)
+
+
+    # Mapping from directory record.recordType to RFC2445 CUTYPE values
+    _cuTypes = {
+        BaseRecordType.user: 'INDIVIDUAL',
+        BaseRecordType.group: 'GROUP',
+        DAVRecordType.resource: 'RESOURCE',
+        DAVRecordType.location: 'ROOM',
+    }
+
+
+    def getCUType(self):
+        return self._cuTypes.get(self.recordType, "UNKNOWN")
+
+
+    @classmethod
+    def fromCUType(cls, cuType):
+        for key, val in cls._cuTypes.iteritems():
+            if val == cuType:
+                return key
+        return None
+
+
+    def applySACLs(self):
+        """
+        Disable calendaring and addressbooks as dictated by SACLs
+        """
+
+        # FIXME: need to re-implement SACLs
+        # if config.EnableSACLs and self.CheckSACL:
+        #     username = self.shortNames[0]
+        #     if self.CheckSACL(username, "calendar") != 0:
+        #         self.log.debug("%s is not enabled for calendaring due to SACL"
+        #                        % (username,))
+        #         self.enabledForCalendaring = False
+        #     if self.CheckSACL(username, "addressbook") != 0:
+        #         self.log.debug("%s is not enabled for addressbooks due to SACL"
+        #                        % (username,))
+        #         self.enabledForAddressBooks = False
+
+
+    @property
+    def displayName(self):
+        return self.fullNames[0]
+
+
+    def cacheToken(self):
+        """
+        Generate a token that can be uniquely used to identify the state of this record for use
+        in a cache.
+        """
+        return hash((
+            self.__class__.__name__,
+            self.service.realmName,
+            self.recordType.name,
+            # self.shortNames, # MOVE2WHO FIXME: is this needed? it's not hashable
+            self.uid,
+            self.hasCalendars,
+        ))
+
+
+    def canonicalCalendarUserAddress(self):
+        """
+            Return a CUA for this record, preferring in this order:
+            urn:uuid: form
+            mailto: form
+            /principals/__uids__/ form
+            first in calendarUserAddresses list (sorted)
+        """
+
+        sortedCuas = sorted(self.calendarUserAddresses)
+
+        for prefix in (
+            "urn:uuid:",
+            "mailto:",
+            "/principals/__uids__/"
+        ):
+            for candidate in sortedCuas:
+                if candidate.startswith(prefix):
+                    return candidate
+
+        # fall back to using the first one
+        return sortedCuas[0]
+
+
+    def enabledAsOrganizer(self):
+        # FIXME:
+        from twistedcaldav.config import config
+
+        if self.recordType == self.service.recordType.user:
+            return True
+        elif self.recordType == self.service.recordType.group:
+            return config.Scheduling.Options.AllowGroupAsOrganizer
+        elif self.recordType == self.service.recordType.location:
+            return config.Scheduling.Options.AllowLocationAsOrganizer
+        elif self.recordType == self.service.recordType.resource:
+            return config.Scheduling.Options.AllowResourceAsOrganizer
+        else:
+            return False
+
+
+    def serverURI(self):
+        """
+        URL of the server hosting this record. Return None if hosted on this server.
+        """
+        # FIXME:
+        from twistedcaldav.config import config
+
+        if config.Servers.Enabled and getattr(self, "serviceNodeUID", None):
+            return Servers.getServerURIById(self.serviceNodeUID)
+        else:
+            return None
+
+
+    def server(self):
+        """
+        Server hosting this record. Return None if hosted on this server.
+        """
+        # FIXME:
+        from twistedcaldav.config import config
+
+        if config.Servers.Enabled and getattr(self, "serviceNodeUID", None):
+            return Servers.getServerById(self.serviceNodeUID)
+        else:
+            return None
+
+
+    def thisServer(self):
+        s = self.server()
+        return s.thisServer if s is not None else True
+
+
+    def isLoginEnabled(self):
+        return self.loginAllowed
+
+
+    def calendarsEnabled(self):
+        # FIXME:
+        from twistedcaldav.config import config
+
+        return config.EnableCalDAV and self.hasCalendars
+
+
+
+    @inlineCallbacks
+    def canAutoSchedule(self, organizer=None):
+        # FIXME:
+        from twistedcaldav.config import config
+
+        if config.Scheduling.Options.AutoSchedule.Enabled:
+            if (
+                config.Scheduling.Options.AutoSchedule.Always or
+                self.autoScheduleMode not in (AutoScheduleMode.none, None) or  # right???
+                (
+                    yield self.autoAcceptFromOrganizer(organizer)
+                )
+            ):
+                if (
+                    self.getCUType() != "INDIVIDUAL" or
+                    config.Scheduling.Options.AutoSchedule.AllowUsers
+                ):
+                    returnValue(True)
+        returnValue(False)
+
+
+    @inlineCallbacks
+    def getAutoScheduleMode(self, organizer):
+        autoScheduleMode = self.autoScheduleMode
+        if (yield self.autoAcceptFromOrganizer(organizer)):
+            autoScheduleMode = AutoScheduleMode.acceptIfFreeDeclineIfBusy
+        returnValue(autoScheduleMode)
+
+
+    @inlineCallbacks
+    def autoAcceptFromOrganizer(self, organizer):
+        try:
+            autoAcceptGroup = self.autoAcceptGroup
+        except AttributeError:
+            autoAcceptGroup = None
+
+        if (
+            organizer is not None and
+            autoAcceptGroup is not None
+        ):
+            service = self.service
+            organizerRecord = yield service.recordWithCalendarUserAddress(organizer)
+            if organizerRecord is not None:
+                autoAcceptGroup = yield service.recordWithUID(autoAcceptGroup)
+                members = yield autoAcceptGroup.expandedMembers()
+                if organizerRecord.uid in ([m.uid for m in members]):
+                    returnValue(True)
+        returnValue(False)
+
+
+    @inlineCallbacks
+    def expandedMembers(self, members=None):
+
+        if members is None:
+            members = set()
+
+        for member in (yield self.members()):
+            if member.recordType == BaseRecordType.user:
+                if member not in members:
+                    members.add(member)
+            yield member.expandedMembers(members)
+
+        returnValue(members)
+
+
+    # For scheduling/freebusy
+    @inlineCallbacks
+    def isProxyFor(self, other):
+        for recordType in (
+            DelegateRecordType.readDelegatorGroup,
+            DelegateRecordType.writeDelegatorGroup,
+        ):
+            delegatorGroup = yield self.service.recordWithShortName(
+                recordType, self.uid
+            )
+            if delegatorGroup:
+                if other in (yield delegatorGroup.members()):
+                    returnValue(True)

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/groups.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/groups.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/groups.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,4 +1,4 @@
-# -*- test-case-name: twext.who.test.test_groups -*-
+# -*- test-case-name: txdav.who.test.test_groups -*-
 ##
 # Copyright (c) 2013 Apple Inc. All rights reserved.
 #
@@ -22,8 +22,6 @@
 from twext.enterprise.dal.record import fromTable
 from twext.enterprise.dal.syntax import Delete, Select
 from twext.enterprise.jobqueue import WorkItem, PeerConnectionPool
-from txdav.who.delegates import allGroupDelegates
-from twext.who.idirectory import RecordType
 from twisted.internet.defer import inlineCallbacks, returnValue
 from txdav.common.datastore.sql_tables import schema
 import datetime
@@ -46,17 +44,14 @@
         # Delete all other work items
         yield Delete(From=self.table, Where=None).on(self.transaction)
 
-        oldGroupCacher = getattr(self.transaction, "_groupCacher", None)
-        newGroupCacher = getattr(self.transaction, "_newGroupCacher", None)
-        if oldGroupCacher is not None or newGroupCacher is not None:
+        groupCacher = getattr(self.transaction, "_groupCacher", None)
+        if groupCacher is not None:
 
             # Schedule next update
 
-            # TODO: Be sure to move updateSeconds to the new cacher
-            # implementation
             notBefore = (
                 datetime.datetime.utcnow() +
-                datetime.timedelta(seconds=oldGroupCacher.updateSeconds)
+                datetime.timedelta(seconds=groupCacher.updateSeconds)
             )
             log.debug(
                 "Scheduling next group cacher update: {when}", when=notBefore
@@ -68,22 +63,13 @@
 
             # New implmementation
             try:
-                newGroupCacher.update(self.transaction)
+                yield groupCacher.update(self.transaction)
             except Exception, e:
                 log.error(
                     "Failed to update new group membership cache ({error})",
                     error=e
                 )
 
-            # Old implmementation
-            try:
-                oldGroupCacher.updateCache()
-            except Exception, e:
-                log.error(
-                    "Failed to update old group membership cache ({error})",
-                    error=e
-                )
-
         else:
             notBefore = (
                 datetime.datetime.utcnow() +
@@ -126,25 +112,28 @@
 
 class GroupRefreshWork(WorkItem, fromTable(schema.GROUP_REFRESH_WORK)):
 
-    group = property(lambda self: self.groupGUID)
+    # Note, the schema has "groupGuid", but really it's a UID.  At some point
+    # we should change the column name.
+    group = property(lambda self: self.groupGuid)
 
     @inlineCallbacks
     def doWork(self):
-
         # Delete all other work items for this group
         yield Delete(
-            From=self.table, Where=(self.table.GROUP_GUID == self.groupGUID)
+            From=self.table, Where=(self.table.GROUP_GUID == self.groupGuid)
         ).on(self.transaction)
 
         groupCacher = getattr(self.transaction, "_groupCacher", None)
         if groupCacher is not None:
 
             try:
-                groupCacher.refreshGroup(self.transaction, self.groupGUID)
+                yield groupCacher.refreshGroup(
+                    self.transaction, self.groupGuid.decode("utf-8")
+                )
             except Exception, e:
                 log.error(
                     "Failed to refresh group {group} {err}",
-                    group=self.groupGUID, err=e
+                    group=self.groupGuid, err=e
                 )
 
         else:
@@ -154,11 +143,11 @@
             )
             log.debug(
                 "Rescheduling group refresh for {group}: {when}",
-                group=self.groupGUID, when=notBefore
+                group=self.groupGuid, when=notBefore
             )
             yield self.transaction.enqueue(
                 GroupRefreshWork,
-                groupGUID=self.groupGUID, notBefore=notBefore
+                groupGuid=self.groupGuid, notBefore=notBefore
             )
 
 
@@ -182,51 +171,26 @@
             )
         ).on(self.transaction)
 
+    # MOVE2WHO
     # TODO: Pull this over from groupcacher branch
 
 
 
- at inlineCallbacks
-def expandedMembers(record, members=None, records=None):
-    """
-    Return the expanded set of member records.  Intermediate groups are not
-    returned in the results, but their members are.
-    """
-    if members is None:
-        members = set()
-    if records is None:
-        records = set()
 
-    if record not in records:
-        records.add(record)
-        for member in (yield record.members()):
-            if member not in records:
-                #TODO:  HACK for old-style XML. FIX
-                if (
-                    member.recordType != RecordType.group and
-                    str(member.recordType) != "groups"
-                ):
-                    members.add(member)
-                yield expandedMembers(member, members, records)
-
-    returnValue(members)
-
-
-
 def diffAssignments(old, new):
     """
     Compare two proxy assignment lists and return their differences in the form
     of two lists -- one for added/updated assignments, and one for removed
     assignments.
 
-    @param old: dictionary of delegator: (readGroupGUID, writeGroupGUID)
+    @param old: dictionary of delegator: (readGroupUID, writeGroupUID)
     @type old: C{dict}
 
-    @param new: dictionary of delegator: (readGroupGUID, writeGroupGUID)
+    @param new: dictionary of delegator: (readGroupUID, writeGroupUID)
     @type new: C{dict}
 
     @return: Tuple of two lists; the first list contains tuples of (delegator,
-        (readGroupGUID, writeGroupGUID)), and represents all the new or updated
+        (readGroupUID, writeGroupUID)), and represents all the new or updated
         assignments.  The second list contains all the delegators which used to
         have a delegate but don't anymore.
     """
@@ -251,13 +215,16 @@
 
     def __init__(
         self, directory,
-        useExternalProxies=False, externalProxiesSource=None
+        updateSeconds=600,
+        useExternalProxies=False,
+        externalProxiesSource=None
     ):
         self.directory = directory
         self.useExternalProxies = useExternalProxies
         if useExternalProxies and externalProxiesSource is None:
             externalProxiesSource = self.directory.getExternalProxyAssignments
         self.externalProxiesSource = externalProxiesSource
+        self.updateSeconds = updateSeconds
 
 
     @inlineCallbacks
@@ -269,19 +236,21 @@
         # yield self.applyExternalAssignments(txn, externalAssignments)
 
         # Figure out which groups matter
-        groupGUIDs = yield self.groupsToRefresh(txn)
+        groupUIDs = yield self.groupsToRefresh(txn)
         self.log.debug(
-            "Number of groups to refresh: {num}", num=len(groupGUIDs)
+            "Number of groups to refresh: {num}", num=len(groupUIDs)
         )
         # For each of those groups, create a per-group refresh work item
-        for groupGUID in groupGUIDs:
+        for groupUID in groupUIDs:
             notBefore = (
                 datetime.datetime.utcnow() +
                 datetime.timedelta(seconds=1)
             )
+            self.log.debug("Enqueuing group refresh for {u}", u=groupUID)
             yield txn.enqueue(
-                GroupRefreshWork, groupGUID=groupGUID, notBefore=notBefore
+                GroupRefreshWork, groupGuid=groupUID, notBefore=notBefore
             )
+            self.log.debug("Enqueued group refresh for {u}", u=groupUID)
 
 
     @inlineCallbacks
@@ -290,84 +259,91 @@
         oldAssignments = (yield txn.externalDelegates())
 
         # external assignments is of the form:
-        # { delegatorGUID: (readDelegateGroupGUID, writeDelegateGroupGUID),
+        # { delegatorUID: (readDelegateGroupUID, writeDelegateGroupUID),
         # }
 
         changed, removed = diffAssignments(oldAssignments, newAssignments)
         if changed:
             for (
-                delegatorGUID, (readDelegateGUID, writeDelegateGUID)
+                delegatorUID, (readDelegateUID, writeDelegateUID)
             ) in changed:
                 readDelegateGroupID = writeDelegateGroupID = None
-                if readDelegateGUID:
+                if readDelegateUID:
                     readDelegateGroupID, _ignore_name, hash = (
-                        yield txn.groupByGUID(readDelegateGUID)
+                        yield txn.groupByUID(readDelegateUID)
                     )
-                if writeDelegateGUID:
+                if writeDelegateUID:
                     writeDelegateGroupID, _ignore_name, hash = (
-                        yield txn.groupByGUID(writeDelegateGUID)
+                        yield txn.groupByUID(writeDelegateUID)
                     )
                 yield txn.assignExternalDelegates(
-                    delegatorGUID, readDelegateGroupID, writeDelegateGroupID,
-                    readDelegateGUID, writeDelegateGUID
+                    delegatorUID, readDelegateGroupID, writeDelegateGroupID,
+                    readDelegateUID, writeDelegateUID
                 )
         if removed:
-            for delegatorGUID in removed:
+            for delegatorUID in removed:
                 yield txn.assignExternalDelegates(
-                    delegatorGUID, None, None, None, None
+                    delegatorUID, None, None, None, None
                 )
 
 
     @inlineCallbacks
-    def refreshGroup(self, txn, groupGUID):
+    def refreshGroup(self, txn, groupUID):
         # Does the work of a per-group refresh work item
-        # Faults in the flattened membership of a group, as GUIDs
+        # Faults in the flattened membership of a group, as UIDs
         # and updates the GROUP_MEMBERSHIP table
-        record = (yield self.directory.recordWithGUID(groupGUID))
-        membershipHashContent = hashlib.md5()
-        members = (yield expandedMembers(record))
-        members = list(members)
-        members.sort(cmp=lambda x, y: cmp(x.guid, y.guid))
-        for member in members:
-            membershipHashContent.update(str(member.guid))
-        membershipHash = membershipHashContent.hexdigest()
-        groupID, _ignore_cachedName, cachedMembershipHash = (
-            yield txn.groupByGUID(groupGUID)
-        )
-
-        if cachedMembershipHash != membershipHash:
-            membershipChanged = True
-            self.log.debug(
-                "Group '{group}' changed", group=record.fullNames[0]
-            )
+        self.log.debug("Faulting in group: {g}", g=groupUID)
+        record = (yield self.directory.recordWithUID(groupUID))
+        if record is None:
+            # FIXME: the group has disappeared from the directory.
+            # How do we want to handle this?
+            self.log.info("Group has disappeared: {g}", g=groupUID)
         else:
-            membershipChanged = False
+            self.log.debug("Got group record: {u}", u=record.uid)
+            membershipHashContent = hashlib.md5()
+            members = yield record.expandedMembers()
+            members = list(members)
+            members.sort(cmp=lambda x, y: cmp(x.uid, y.uid))
+            for member in members:
+                membershipHashContent.update(str(member.uid))
+            membershipHash = membershipHashContent.hexdigest()
+            groupID, _ignore_cachedName, cachedMembershipHash = (
+                yield txn.groupByUID(groupUID)
+            )
 
-        yield txn.updateGroup(groupGUID, record.fullNames[0], membershipHash)
+            if cachedMembershipHash != membershipHash:
+                membershipChanged = True
+                self.log.debug(
+                    "Group '{group}' changed", group=record.fullNames[0]
+                )
+            else:
+                membershipChanged = False
 
-        if membershipChanged:
-            newMemberGUIDs = set()
-            for member in members:
-                newMemberGUIDs.add(member.guid)
-            yield self.synchronizeMembers(txn, groupID, newMemberGUIDs)
+            yield txn.updateGroup(groupUID, record.fullNames[0], membershipHash)
 
-        yield self.scheduleEventReconciliations(txn, groupID, groupGUID)
+            if membershipChanged:
+                newMemberUIDs = set()
+                for member in members:
+                    newMemberUIDs.add(member.uid)
+                yield self.synchronizeMembers(txn, groupID, newMemberUIDs)
 
+            yield self.scheduleEventReconciliations(txn, groupID, groupUID)
 
+
     @inlineCallbacks
-    def synchronizeMembers(self, txn, groupID, newMemberGUIDs):
+    def synchronizeMembers(self, txn, groupID, newMemberUIDs):
         numRemoved = numAdded = 0
-        cachedMemberGUIDs = (yield txn.membersOfGroup(groupID))
+        cachedMemberUIDs = (yield txn.membersOfGroup(groupID))
 
-        for memberGUID in cachedMemberGUIDs:
-            if memberGUID not in newMemberGUIDs:
+        for memberUID in cachedMemberUIDs:
+            if memberUID not in newMemberUIDs:
                 numRemoved += 1
-                yield txn.removeMemberFromGroup(memberGUID, groupID)
+                yield txn.removeMemberFromGroup(memberUID, groupID)
 
-        for memberGUID in newMemberGUIDs:
-            if memberGUID not in cachedMemberGUIDs:
+        for memberUID in newMemberUIDs:
+            if memberUID not in cachedMemberUIDs:
                 numAdded += 1
-                yield txn.addMemberToGroup(memberGUID, groupID)
+                yield txn.addMemberToGroup(memberUID, groupID)
 
         returnValue((numAdded, numRemoved))
 
@@ -378,23 +354,23 @@
         The members of the given group as recorded in the db
         """
         members = set()
-        memberGUIDs = (yield txn.membersOfGroup(groupID))
-        for guid in memberGUIDs:
-            record = (yield self.directory.recordWithGUID(guid))
+        memberUIDs = (yield txn.membersOfGroup(groupID))
+        for uid in memberUIDs:
+            record = (yield self.directory.recordWithUID(uid))
             if record is not None:
                 members.add(record)
         returnValue(members)
 
 
-    def cachedGroupsFor(self, txn, guid):
+    def cachedGroupsFor(self, txn, uid):
         """
-        The IDs of the groups the guid is a member of
+        The UIDs of the groups the uid is a member of
         """
-        return txn.groupsFor(guid)
+        return txn.groupsFor(uid)
 
 
     @inlineCallbacks
-    def scheduleEventReconciliations(self, txn, groupID, groupGUID):
+    def scheduleEventReconciliations(self, txn, groupID, groupUID):
         """
         Find all events who have this groupID as an attendee and create
         work items for them.
@@ -415,29 +391,29 @@
             )
             log.debug(
                 "scheduling group reconciliation for "
-                "({eventID}, {groupID}, {groupGUID}): {when}",
+                "({eventID}, {groupID}, {groupUID}): {when}",
                 eventID=eventID,
                 groupID=groupID,
-                groupGUID=groupGUID,
+                groupUID=groupUID,
                 when=notBefore)
 
             yield txn.enqueue(
                 GroupAttendeeReconciliationWork,
                 eventID=eventID,
                 groupID=groupID,
-                groupGUID=groupGUID,
+                groupGuid=groupUID,
                 notBefore=notBefore
             )
 
 
     @inlineCallbacks
     def groupsToRefresh(self, txn):
-        delegatedGUIDs = set((yield allGroupDelegates(txn)))
+        delegatedUIDs = set((yield txn.allGroupDelegates()))
         self.log.info(
-            "There are {count} group delegates", count=len(delegatedGUIDs)
+            "There are {count} group delegates", count=len(delegatedUIDs)
         )
 
-        attendeeGroupGUIDs = set()
+        attendeeGroupUIDs = set()
 
         # get all groups from events
         groupAttendee = schema.GROUP_ATTENDEE
@@ -447,7 +423,7 @@
         ).on(txn)
         groupIDs = set([row[0] for row in rows])
 
-        # get groupGUIDs
+        # get groupUIDs
         if groupIDs:
             gr = schema.GROUPS
             rows = yield Select(
@@ -455,6 +431,8 @@
                 From=gr,
                 Where=gr.GROUP_ID.In(groupIDs)
             ).on(txn)
-            attendeeGroupGUIDs = set([row[0] for row in rows])
+            attendeeGroupUIDs = set([row[0] for row in rows])
 
-        returnValue(delegatedGUIDs.union(attendeeGroupGUIDs))
+        # FIXME: is this a good place to clear out unreferenced groups?
+
+        returnValue(delegatedUIDs.union(attendeeGroupUIDs))

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/idirectory.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/idirectory.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/idirectory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -23,6 +23,9 @@
 """
 
 __all__ = [
+    "AutoScheduleMode",
+    "RecordType",
+    "FieldName",
 ]
 
 from twisted.python.constants import Names, NamedConstant
@@ -152,3 +155,26 @@
     autoAcceptGroup = NamedConstant()
     autoAcceptGroup.description = u"auto-accept group"
     autoAcceptGroup.valueType = BaseFieldName.valueType(BaseFieldName.uid)
+
+    # For "locations", i.e., scheduled spaces:
+
+    associatedAddress = NamedConstant()
+    associatedAddress.description = u"associated address UID"
+
+    capacity = NamedConstant()
+    capacity.description = u"room capacity"
+    capacity.valueType = int
+
+    floor = NamedConstant()
+    floor.description = u"building floor"
+
+    # For "addresses", i.e., non-scheduled areas containing locations:
+
+    abbreviatedName = NamedConstant()
+    abbreviatedName.description = u"abbreviated name"
+
+    geographicLocation = NamedConstant()
+    geographicLocation.description = u"geographic location URI"
+
+    streetAddress = NamedConstant()
+    streetAddress.description = u"street address"

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/accounts.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/accounts.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/accounts.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,174 +18,339 @@
 
 <!DOCTYPE accounts SYSTEM "accounts.dtd">
 
-<accounts realm="Test Realm">
-  <user>
+<directory realm="Test Realm">
+  <record type="user">
     <uid>admin</uid>
-    <guid>admin</guid>
+    <short-name>admin</short-name>
     <password>admin</password>
-    <name>Super User</name>
-    <first-name>Super</first-name>
-    <last-name>User</last-name>
-  </user>
-  <user>
+    <full-name>Super User</full-name>
+  </record>
+  <record type="user">
     <uid>apprentice</uid>
-    <guid>apprentice</guid>
+    <short-name>apprentice</short-name>
     <password>apprentice</password>
-    <name>Apprentice Super User</name>
-    <first-name>Apprentice</first-name>
-    <last-name>Super User</last-name>
-  </user>
-  <user>
+    <full-name>Apprentice Super User</full-name>
+  </record>
+  <record type="user">
     <uid>wsanchez</uid>
-    <guid>wsanchez</guid>
-    <email-address>wsanchez at example.com</email-address>
+    <short-name>wsanchez</short-name>
+    <email>wsanchez at example.com</email>
     <password>test</password>
-    <name>Wilfredo Sanchez Vega</name>
-    <first-name>Wilfredo</first-name>
-    <last-name>Sanchez Vega</last-name>
-  </user>
-  <user>
+    <full-name>Wilfredo Sanchez Vega</full-name>
+  </record>
+  <record type="user">
     <uid>cdaboo</uid>
-    <guid>cdaboo</guid>
-    <email-address>cdaboo at example.com</email-address>
+    <short-name>cdaboo</short-name>
+    <email>cdaboo at example.com</email>
     <password>test</password>
-    <name>Cyrus Daboo</name>
-    <first-name>Cyrus</first-name>
-    <last-name>Daboo</last-name>
-  </user>
-  <user>
+    <full-name>cyrus Daboo</full-name>
+  </record>
+  <record type="user">
     <uid>sagen</uid>
-    <guid>sagen</guid>
-    <email-address>sagen at example.com</email-address>
+    <short-name>sagen</short-name>
+    <email>sagen at example.com</email>
     <password>test</password>
-    <name>Morgen Sagen</name>
-    <first-name>Morgen</first-name>
-    <last-name>Sagen</last-name>
-  </user>
-  <user>
+    <full-name>Morgen Sagen</full-name>
+  </record>
+  <record type="user">
     <uid>dre</uid>
-    <guid>andre</guid>
-    <email-address>dre at example.com</email-address>
+    <short-name>andre</short-name>
+    <email>dre at example.com</email>
     <password>test</password>
-    <name>Andre LaBranche</name>
-    <first-name>Andre</first-name>
-    <last-name>LaBranche</last-name>
-  </user>
-  <user>
+    <full-name>Andre LaBranche</full-name>
+  </record>
+  <record type="user">
     <uid>glyph</uid>
-    <guid>glyph</guid>
-    <email-address>glyph at example.com</email-address>
+    <short-name>glyph</short-name>
+    <email>glyph at example.com</email>
     <password>test</password>
-    <name>Glyph Lefkowitz</name>
-    <first-name>Glyph</first-name>
-    <last-name>Lefkowitz</last-name>
-  </user>
-  <user>
+    <full-name>Glyph Lefkowitz</full-name>
+  </record>
+  <record type="user">
     <uid>i18nuser</uid>
-    <guid>i18nuser</guid>
-    <email-address>i18nuser at example.com</email-address>
+    <short-name>i18nuser</short-name>
+    <email>i18nuser at example.com</email>
     <password>i18nuser</password>
-    <name>まだ</name>
-    <first-name>ま</first-name>
-    <last-name>だ</last-name>
-  </user>
+    <full-name>まだ</full-name>
+  </record>
+
+  <!-- twext.who xml doesn't (yet) support repeat
   <user repeat="101">
     <uid>user%02d</uid>
     <uid>User %02d</uid>
-    <guid>user%02d</guid>
+    <short-name>user%02d</short-name>
     <password>user%02d</password>
-    <name>User %02d</name>
-    <first-name>User</first-name>
-    <last-name>%02d</last-name>
-    <email-address>user%02d at example.com</email-address>
-  </user>
+    <full-name>User %02d</full-name>
+    <email>user%02d at example.com</email>
+  </record>
   <user repeat="10">
     <uid>public%02d</uid>
-    <guid>public%02d</guid>
+    <short-name>public%02d</short-name>
     <password>public%02d</password>
-    <name>Public %02d</name>
-    <first-name>Public</first-name>
-    <last-name>%02d</last-name>
-  </user>
-  <group>
+    <full-name>Public %02d</full-name>
+  </record>
+  -->
+  <record type="user">
+    <short-name>user01</short-name>
+    <uid>user01</uid>
+    <password>user01</password>
+    <full-name>User 01</full-name>
+    <email>user01 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user02</short-name>
+    <uid>user02</uid>
+    <password>user02</password>
+    <full-name>User 02</full-name>
+    <email>user02 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user03</short-name>
+    <uid>user03</uid>
+    <password>user03</password>
+    <full-name>User 03</full-name>
+    <email>user03 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user04</short-name>
+    <uid>user04</uid>
+    <password>user04</password>
+    <full-name>User 04</full-name>
+    <email>user04 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user05</short-name>
+    <uid>user05</uid>
+    <password>user05</password>
+    <full-name>User 05</full-name>
+    <email>user05 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user06</short-name>
+    <uid>user06</uid>
+    <password>user06</password>
+    <full-name>User 06</full-name>
+    <email>user06 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user07</short-name>
+    <uid>user07</uid>
+    <password>user07</password>
+    <full-name>User 07</full-name>
+    <email>user07 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user08</short-name>
+    <uid>user08</uid>
+    <password>user08</password>
+    <full-name>User 08</full-name>
+    <email>user08 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user09</short-name>
+    <uid>user09</uid>
+    <password>user09</password>
+    <full-name>User 09</full-name>
+    <email>user09 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user10</short-name>
+    <uid>user10</uid>
+    <password>user10</password>
+    <full-name>User 10</full-name>
+    <email>user10 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user11</short-name>
+    <uid>user11</uid>
+    <password>user11</password>
+    <full-name>User 11</full-name>
+    <email>user11 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user12</short-name>
+    <uid>user12</uid>
+    <password>user12</password>
+    <full-name>User 12</full-name>
+    <email>user12 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user13</short-name>
+    <uid>user13</uid>
+    <password>user13</password>
+    <full-name>User 13</full-name>
+    <email>user13 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user14</short-name>
+    <uid>user14</uid>
+    <password>user14</password>
+    <full-name>User 14</full-name>
+    <email>user14 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user15</short-name>
+    <uid>user15</uid>
+    <password>user15</password>
+    <full-name>User 15</full-name>
+    <email>user15 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user16</short-name>
+    <uid>user16</uid>
+    <password>user16</password>
+    <full-name>User 16</full-name>
+    <email>user16 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user17</short-name>
+    <uid>user17</uid>
+    <password>user17</password>
+    <full-name>User 17</full-name>
+    <email>user17 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user18</short-name>
+    <uid>user18</uid>
+    <password>user18</password>
+    <full-name>User 18</full-name>
+    <email>user18 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user19</short-name>
+    <uid>user19</uid>
+    <password>user19</password>
+    <full-name>User 19</full-name>
+    <email>user19 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user20</short-name>
+    <uid>user20</uid>
+    <password>user20</password>
+    <full-name>User 20</full-name>
+    <email>user20 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user21</short-name>
+    <uid>user21</uid>
+    <password>user21</password>
+    <full-name>User 21</full-name>
+    <email>user21 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user22</short-name>
+    <uid>user22</uid>
+    <password>user22</password>
+    <full-name>User 22</full-name>
+    <email>user22 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user23</short-name>
+    <uid>user23</uid>
+    <password>user23</password>
+    <full-name>User 23</full-name>
+    <email>user23 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user24</short-name>
+    <uid>user24</uid>
+    <password>user24</password>
+    <full-name>User 24</full-name>
+    <email>user24 at example.com</email>
+  </record>
+
+  <record type="user">
+    <short-name>user25</short-name>
+    <uid>user25</uid>
+    <password>user25</password>
+    <full-name>User 25</full-name>
+    <email>user25 at example.com</email>
+  </record>
+
+  <record type="group">
     <uid>group01</uid>
-    <guid>group01</guid>
+    <short-name>group01</short-name>
     <password>group01</password>
-    <name>Group 01</name>
-    <email-address>group01 at example.com</email-address>
-    <members>
-      <member type="users">user01</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Group 01</full-name>
+      <member-uid type="users">user01</member-uid>
+  </record>
+  <record type="group">
     <uid>group02</uid>
-    <guid>group02</guid>
+    <short-name>group02</short-name>
     <password>group02</password>
-    <name>Group 02</name>
-    <email-address>group02 at example.com</email-address>
-    <members>
-      <member type="users">user06</member>
-      <member type="users">user07</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Group 02</full-name>
+      <member-uid type="users">user06</member-uid>
+      <member-uid type="users">user07</member-uid>
+  </record>
+  <record type="group">
     <uid>group03</uid>
-    <guid>group03</guid>
+    <short-name>group03</short-name>
     <password>group03</password>
-    <name>Group 03</name>
-    <members>
-      <member type="users">user08</member>
-      <member type="users">user09</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Group 03</full-name>
+      <member-uid type="users">user08</member-uid>
+      <member-uid type="users">user09</member-uid>
+  </record>
+  <record type="group">
     <uid>group04</uid>
-    <guid>group04</guid>
+    <short-name>group04</short-name>
     <password>group04</password>
-    <name>Group 04</name>
-    <members>
-      <member type="groups">group02</member>
-      <member type="groups">group03</member>
-      <member type="users">user10</member>
-    </members>
-  </group>
-  <group> <!-- delegategroup -->
+    <full-name>Group 04</full-name>
+      <member-uid type="groups">group02</member-uid>
+      <member-uid type="groups">group03</member-uid>
+      <member-uid type="users">user10</member-uid>
+  </record>
+  <record type="group"> <!-- delegategroup -->
     <uid>group05</uid>
-    <guid>group05</guid>
+    <short-name>group05</short-name>
     <password>group05</password>
-    <name>Group 05</name>
-    <members>
-      <member type="groups">group06</member>
-      <member type="users">user20</member>
-    </members>
-  </group>
-  <group> <!-- delegatesubgroup -->
+    <full-name>Group 05</full-name>
+      <member-uid type="groups">group06</member-uid>
+      <member-uid type="users">user20</member-uid>
+  </record>
+  <record type="group"> <!-- delegatesubgroup -->
     <uid>group06</uid>
-    <guid>group06</guid>
+    <short-name>group06</short-name>
     <password>group06</password>
-    <name>Group 06</name>
-    <members>
-      <member type="users">user21</member>
-    </members>
-  </group>
-  <group> <!-- readonlydelegategroup -->
+    <full-name>Group 06</full-name>
+      <member-uid type="users">user21</member-uid>
+  </record>
+  <record type="group"> <!-- readonlydelegategroup -->
     <uid>group07</uid>
-    <guid>group07</guid>
+    <short-name>group07</short-name>
     <password>group07</password>
-    <name>Group 07</name>
-    <members>
-      <member type="users">user22</member>
-      <member type="users">user23</member>
-      <member type="users">user24</member>
-    </members>
-  </group>
-  <group>
+    <full-name>Group 07</full-name>
+      <member-uid type="users">user22</member-uid>
+      <member-uid type="users">user23</member-uid>
+      <member-uid type="users">user24</member-uid>
+  </record>
+  <record type="group">
     <uid>disabledgroup</uid>
-    <guid>disabledgroup</guid>
+    <short-name>disabledgroup</short-name>
     <password>disabledgroup</password>
-    <name>Disabled Group</name>
-    <members>
-      <member type="users">user01</member>
-    </members>
-  </group>
-</accounts>
+    <full-name>Disabled Group</full-name>
+      <member-uid type="users">user01</member-uid>
+  </record>
+</directory>

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/augments.xml (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/test/accounts/augments.xml)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/augments.xml	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/augments.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE augments SYSTEM "augments.dtd">
+
+<augments>
+  <record>
+    <uid>Default</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+  </record>
+  <record repeat="10">
+    <uid>location%02d</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record repeat="4">
+    <uid>resource%02d</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>resource05</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>none</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource06</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-always</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource07</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>decline-always</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource08</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>accept-if-free</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource09</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>decline-if-busy</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource10</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>automatic</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>resource11</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>decline-always</auto-schedule-mode>
+    <auto-accept-group>group01</auto-accept-group>
+  </record>
+  <record repeat="10">
+    <uid>group%02d</uid>
+    <enable>true</enable>
+  </record>
+  <record>
+    <uid>disabledgroup</uid>
+    <enable>false</enable>
+  </record>
+  <record>
+    <uid>delegatedroom</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>false</enable-addressbook>
+    <auto-schedule>false</auto-schedule>
+  </record>
+  <record>
+    <uid>03DFF660-8BCC-4198-8588-DD77F776F518</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>80689D41-DAF8-4189-909C-DB017B271892</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+    <auto-accept-group>group01</auto-accept-group>
+  </record>
+  <record>
+    <uid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+  </record>
+  <record>
+    <uid>6F9EE33B-78F6-481B-9289-3D0812FF0D64</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>false</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>false</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>false</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>06E3BDCB-9C19-485A-B14E-F146A80ADDC6</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+  <record>
+    <uid>4D66A20A-1437-437D-8069-2F14E8322234</uid>
+    <enable>true</enable>
+    <enable-calendar>true</enable-calendar>
+    <enable-addressbook>true</enable-addressbook>
+    <enable-login>true</enable-login>
+    <auto-schedule>true</auto-schedule>
+    <auto-schedule-mode>default</auto-schedule-mode>
+  </record>
+</augments>

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/resources.xml
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/resources.xml	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/accounts/resources.xml	2014-04-04 17:20:27 UTC (rev 13158)
@@ -1,34 +1,273 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2006-2014 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 "accounts.dtd">
-
-<accounts realm="Test Realm">
-  <location repeat="10">
-    <uid>location%02d</uid>
-    <guid>location%02d</guid>
-    <password>location%02d</password>
-    <name>Room %02d</name>
-  </location>
-  <resource repeat="10">
-    <uid>resource%02d</uid>
-    <guid>resource%02d</guid>
-    <password>resource%02d</password>
-    <name>Resource %02d</name>
-  </resource>
-</accounts>
+<directory realm="Test Realm">
+  <record type="location">
+    <short-name>fantastic</short-name>
+    <uid>4D66A20A-1437-437D-8069-2F14E8322234</uid>
+    <full-name>Fantastic Conference Room</full-name>
+    <extras>
+      <associatedAddress>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</associatedAddress>
+    </extras>
+  </record>
+  <record type="location">
+    <short-name>jupiter</short-name>
+    <uid>jupiter</uid>
+    <full-name>Jupiter Conference Room, Building 2, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>uranus</short-name>
+    <uid>uranus</uid>
+    <full-name>Uranus Conference Room, Building 3, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>morgensroom</short-name>
+    <uid>03DFF660-8BCC-4198-8588-DD77F776F518</uid>
+    <full-name>Morgen's Room</full-name>
+  </record>
+  <record type="location">
+    <short-name>mercury</short-name>
+    <uid>mercury</uid>
+    <full-name>Mercury Conference Room, Building 1, 2nd Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>location09</short-name>
+    <uid>location09</uid>
+    <full-name>Room 09</full-name>
+  </record>
+  <record type="location">
+    <short-name>location08</short-name>
+    <uid>location08</uid>
+    <full-name>Room 08</full-name>
+  </record>
+  <record type="location">
+    <short-name>location07</short-name>
+    <uid>location07</uid>
+    <full-name>Room 07</full-name>
+  </record>
+  <record type="location">
+    <short-name>location06</short-name>
+    <uid>location06</uid>
+    <full-name>Room 06</full-name>
+  </record>
+  <record type="location">
+    <short-name>location05</short-name>
+    <uid>location05</uid>
+    <full-name>Room 05</full-name>
+  </record>
+  <record type="location">
+    <short-name>location04</short-name>
+    <uid>location04</uid>
+    <full-name>Room 04</full-name>
+  </record>
+  <record type="location">
+    <short-name>location03</short-name>
+    <uid>location03</uid>
+    <full-name>Room 03</full-name>
+  </record>
+  <record type="location">
+    <short-name>location02</short-name>
+    <uid>location02</uid>
+    <full-name>Room 02</full-name>
+  </record>
+  <record type="location">
+    <short-name>location01</short-name>
+    <uid>location01</uid>
+    <full-name>Room 01</full-name>
+  </record>
+  <record type="location">
+    <short-name>delegatedroom</short-name>
+    <uid>delegatedroom</uid>
+    <full-name>Delegated Conference Room</full-name>
+  </record>
+  <record type="location">
+    <short-name>mars</short-name>
+    <uid>redplanet</uid>
+    <full-name>Mars Conference Room, Building 1, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>sharissroom</short-name>
+    <uid>80689D41-DAF8-4189-909C-DB017B271892</uid>
+    <full-name>Shari's Room</full-name>
+    <extras>
+      <associatedAddress>6F9EE33B-78F6-481B-9289-3D0812FF0D64</associatedAddress>
+    </extras>
+  </record>
+  <record type="location">
+    <short-name>pluto</short-name>
+    <uid>pluto</uid>
+    <full-name>Pluto Conference Room, Building 2, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>saturn</short-name>
+    <uid>saturn</uid>
+    <full-name>Saturn Conference Room, Building 2, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>location10</short-name>
+    <uid>location10</uid>
+    <full-name>Room 10</full-name>
+  </record>
+  <record type="location">
+    <short-name>pretend</short-name>
+    <uid>06E3BDCB-9C19-485A-B14E-F146A80ADDC6</uid>
+    <full-name>Pretend Conference Room</full-name>
+    <extras>
+      <associatedAddress>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</associatedAddress>
+    </extras>
+  </record>
+  <record type="location">
+    <short-name>neptune</short-name>
+    <uid>neptune</uid>
+    <full-name>Neptune Conference Room, Building 2, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>Earth</short-name>
+    <uid>Earth</uid>
+    <full-name>Earth Conference Room, Building 1, 1st Floor</full-name>
+  </record>
+  <record type="location">
+    <short-name>venus</short-name>
+    <uid>venus</uid>
+    <full-name>Venus Conference Room, Building 1, 2nd Floor</full-name>
+  </record>
+  <record type="resource">
+    <short-name>sharisotherresource</short-name>
+    <uid>CCE95217-A57B-481A-AC3D-FEC9AB6CE3A9</uid>
+    <full-name>Shari's Other Resource</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource15</short-name>
+    <uid>resource15</uid>
+    <full-name>Resource 15</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource14</short-name>
+    <uid>resource14</uid>
+    <full-name>Resource 14</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource17</short-name>
+    <uid>resource17</uid>
+    <full-name>Resource 17</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource16</short-name>
+    <uid>resource16</uid>
+    <full-name>Resource 16</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource11</short-name>
+    <uid>resource11</uid>
+    <full-name>Resource 11</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource10</short-name>
+    <uid>resource10</uid>
+    <full-name>Resource 10</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource13</short-name>
+    <uid>resource13</uid>
+    <full-name>Resource 13</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource12</short-name>
+    <uid>resource12</uid>
+    <full-name>Resource 12</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource19</short-name>
+    <uid>resource19</uid>
+    <full-name>Resource 19</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource18</short-name>
+    <uid>resource18</uid>
+    <full-name>Resource 18</full-name>
+  </record>
+  <record type="resource">
+    <short-name>sharisresource</short-name>
+    <uid>C38BEE7A-36EE-478C-9DCB-CBF4612AFE65</uid>
+    <full-name>Shari's Resource</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource20</short-name>
+    <uid>resource20</uid>
+    <full-name>Resource 20</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource06</short-name>
+    <uid>resource06</uid>
+    <full-name>Resource 06</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource07</short-name>
+    <uid>resource07</uid>
+    <full-name>Resource 07</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource04</short-name>
+    <uid>resource04</uid>
+    <full-name>Resource 04</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource05</short-name>
+    <uid>resource05</uid>
+    <full-name>Resource 05</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource02</short-name>
+    <uid>resource02</uid>
+    <full-name>Resource 02</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource03</short-name>
+    <uid>resource03</uid>
+    <full-name>Resource 03</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource01</short-name>
+    <uid>resource01</uid>
+    <full-name>Resource 01</full-name>
+  </record>
+  <record type="resource">
+    <short-name>sharisotherresource1</short-name>
+    <uid>0CE0BF31-5F9E-4801-A489-8C70CF287F5F</uid>
+    <full-name>Shari's Other Resource1</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource08</short-name>
+    <uid>resource08</uid>
+    <full-name>Resource 08</full-name>
+  </record>
+  <record type="resource">
+    <short-name>resource09</short-name>
+    <uid>resource09</uid>
+    <full-name>Resource 09</full-name>
+  </record>
+  <record type="address">
+    <short-name>testaddress1</short-name>
+    <uid>6F9EE33B-78F6-481B-9289-3D0812FF0D64</uid>
+    <full-name>Test Address One</full-name>
+    <extras>
+      <streetAddress>20300 Stevens Creek Blvd, Cupertino, CA 95014</streetAddress>
+      <geo>37.322281,-122.028345</geo>
+    </extras>
+  </record>
+  <record type="address">
+    <short-name>il2</short-name>
+    <uid>63A2F949-2D8D-4C8D-B8A5-DCF2A94610F3</uid>
+    <full-name>IL2</full-name>
+    <extras>
+      <streetAddress>2 Infinite Loop, Cupertino, CA 95014</streetAddress>
+      <geo>37.332633,-122.030502</geo>
+    </extras>
+  </record>
+  <record type="address">
+    <short-name>il1</short-name>
+    <uid>76E7ECA6-08BC-4AE7-930D-F2E7453993A5</uid>
+    <full-name>IL1</full-name>
+    <extras>
+      <streetAddress>1 Infinite Loop, Cupertino, CA 95014</streetAddress>
+      <geo>37.331741,-122.030333</geo>
+    </extras>
+  </record>
+</directory>

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_augment.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/test/test_augment.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_augment.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_augment.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,62 @@
+##
+# Copyright (c) 2014 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.
+##
+
+"""
+Augment service tests
+"""
+
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.test.util import StoreTestCase
+from txdav.who.groups import GroupCacher
+
+
+class AugmentTest(StoreTestCase):
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(AugmentTest, self).setUp()
+        self.groupCacher = GroupCacher(self.directory)
+
+
+    @inlineCallbacks
+    def test_groups(self):
+        """
+        Make sure augmented record groups( ) returns only the groups that have
+        been refreshed.
+        """
+
+        store = self.storeUnderTest()
+
+        txn = store.newTransaction()
+        yield self.groupCacher.refreshGroup(txn, u"__top_group_1__")
+        yield txn.commit()
+        record = yield self.directory.recordWithUID(u"__sagen1__")
+        groups = yield record.groups()
+        self.assertEquals(
+            set(["__top_group_1__"]),
+            set([g.uid for g in groups])
+        )
+
+        txn = store.newTransaction()
+        yield self.groupCacher.refreshGroup(txn, u"__sub_group_1__")
+        yield txn.commit()
+
+        record = yield self.directory.recordWithUID(u"__sagen1__")
+        groups = yield record.groups()
+        self.assertEquals(
+            set(["__top_group_1__", "__sub_group_1__"]),
+            set([g.uid for g in groups])
+        )

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_delegates.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_delegates.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_delegates.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -19,14 +19,13 @@
 """
 
 from txdav.who.delegates import (
-    addDelegate, removeDelegate, delegatesOf, delegatedTo, allGroupDelegates
+    addDelegate, removeDelegate, delegatesOf, delegatedTo,
+    RecordType as DelegateRecordType
 )
 from txdav.who.groups import GroupCacher
 from twext.who.idirectory import RecordType
-from twext.who.test.test_xml import xmlService
 from twisted.internet.defer import inlineCallbacks
 from twistedcaldav.test.util import StoreTestCase
-from uuid import UUID
 
 
 class DelegationTest(StoreTestCase):
@@ -34,40 +33,83 @@
     @inlineCallbacks
     def setUp(self):
         yield super(DelegationTest, self).setUp()
-        self.xmlService = xmlService(self.mktemp(), xmlData=testXMLConfig)
-        self.groupCacher = GroupCacher(self.xmlService)
+        self.store = self.storeUnderTest()
+        self.groupCacher = GroupCacher(self.directory)
 
 
     @inlineCallbacks
     def test_directDelegation(self):
-        store = self.storeUnderTest()
-        txn = store.newTransaction()
+        txn = self.store.newTransaction()
 
-        delegator = yield self.xmlService.recordWithUID(u"__wsanchez__")
-        delegate1 = yield self.xmlService.recordWithUID(u"__sagen__")
-        delegate2 = yield self.xmlService.recordWithUID(u"__cdaboo__")
+        delegator = yield self.directory.recordWithUID(u"__wsanchez1__")
+        delegate1 = yield self.directory.recordWithUID(u"__sagen1__")
+        delegate2 = yield self.directory.recordWithUID(u"__cdaboo1__")
 
         # Add 1 delegate
         yield addDelegate(txn, delegator, delegate1, True)
         delegates = (yield delegatesOf(txn, delegator, True))
-        self.assertEquals(["sagen"], [d.shortNames[0] for d in delegates])
+        self.assertEquals([u"__sagen1__"], [d.uid for d in delegates])
         delegators = (yield delegatedTo(txn, delegate1, True))
-        self.assertEquals(["wsanchez"], [d.shortNames[0] for d in delegators])
+        self.assertEquals([u"__wsanchez1__"], [d.uid for d in delegators])
 
+        yield txn.commit()  # So delegateService will see the changes
+        txn = self.store.newTransaction()
+
+        # The "proxy-write" pseudoGroup will have one member
+        pseudoGroup = yield self.directory.recordWithShortName(
+            DelegateRecordType.writeDelegateGroup,
+            u"__wsanchez1__"
+        )
+        self.assertEquals(pseudoGroup.uid, u"__wsanchez1__#calendar-proxy-write")
+        self.assertEquals(
+            [r.uid for r in (yield pseudoGroup.members())],
+            [u"__sagen1__"]
+        )
+        # The "proxy-read" pseudoGroup will have no members
+        pseudoGroup = yield self.directory.recordWithShortName(
+            DelegateRecordType.readDelegateGroup,
+            u"__wsanchez1__"
+        )
+        self.assertEquals(pseudoGroup.uid, u"__wsanchez1__#calendar-proxy-read")
+        self.assertEquals(
+            [r.uid for r in (yield pseudoGroup.members())],
+            []
+        )
+        # The "proxy-write-for" pseudoGroup will have one member
+        pseudoGroup = yield self.directory.recordWithShortName(
+            DelegateRecordType.writeDelegatorGroup,
+            u"__sagen1__"
+        )
+        self.assertEquals(pseudoGroup.uid, u"__sagen1__#calendar-proxy-write-for")
+        self.assertEquals(
+            [r.uid for r in (yield pseudoGroup.members())],
+            [u"__wsanchez1__"]
+        )
+        # The "proxy-read-for" pseudoGroup will have no members
+        pseudoGroup = yield self.directory.recordWithShortName(
+            DelegateRecordType.readDelegatorGroup,
+            u"__sagen1__"
+        )
+        self.assertEquals(pseudoGroup.uid, u"__sagen1__#calendar-proxy-read-for")
+        self.assertEquals(
+            [r.uid for r in (yield pseudoGroup.members())],
+            []
+        )
+
         # Add another delegate
         yield addDelegate(txn, delegator, delegate2, True)
         delegates = (yield delegatesOf(txn, delegator, True))
         self.assertEquals(
-            set(["sagen", "cdaboo"]),
-            set([d.shortNames[0] for d in delegates])
+            set([u"__sagen1__", u"__cdaboo1__"]),
+            set([d.uid for d in delegates])
         )
         delegators = (yield delegatedTo(txn, delegate2, True))
-        self.assertEquals(["wsanchez"], [d.shortNames[0] for d in delegators])
+        self.assertEquals([u"__wsanchez1__"], [d.uid for d in delegators])
 
         # Remove 1 delegate
         yield removeDelegate(txn, delegator, delegate1, True)
         delegates = (yield delegatesOf(txn, delegator, True))
-        self.assertEquals(["cdaboo"], [d.shortNames[0] for d in delegates])
+        self.assertEquals([u"__cdaboo1__"], [d.uid for d in delegates])
         delegators = (yield delegatedTo(txn, delegate1, True))
         self.assertEquals(0, len(delegators))
 
@@ -78,162 +120,116 @@
         delegators = (yield delegatedTo(txn, delegate2, True))
         self.assertEquals(0, len(delegators))
 
+        yield txn.commit()  # So delegateService will see the changes
 
+        # Now set delegate assignments by using pseudoGroup.setMembers()
+        pseudoGroup = yield self.directory.recordWithShortName(
+            DelegateRecordType.writeDelegateGroup,
+            u"__wsanchez1__"
+        )
+        yield pseudoGroup.setMembers([delegate1, delegate2])
+
+        # Verify the assignments were made
+        txn = self.store.newTransaction()
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(
+            set([u"__sagen1__", u"__cdaboo1__"]),
+            set([d.uid for d in delegates])
+        )
+        yield txn.commit()
+
+        # Set a different group of assignments:
+        yield pseudoGroup.setMembers([delegate2])
+
+        # Verify the assignments were made
+        txn = self.store.newTransaction()
+        delegates = (yield delegatesOf(txn, delegator, True))
+        self.assertEquals(
+            set([u"__cdaboo1__"]),
+            set([d.uid for d in delegates])
+        )
+        yield txn.commit()
+
+
     @inlineCallbacks
     def test_indirectDelegation(self):
-        store = self.storeUnderTest()
-        txn = store.newTransaction()
+        txn = self.store.newTransaction()
 
-        delegator = yield self.xmlService.recordWithUID(u"__wsanchez__")
-        delegate1 = yield self.xmlService.recordWithUID(u"__sagen__")
-        group1 = yield self.xmlService.recordWithUID(u"__top_group_1__")
-        group2 = yield self.xmlService.recordWithUID(u"__sub_group_1__")
+        delegator = yield self.directory.recordWithUID(u"__wsanchez1__")
+        delegate1 = yield self.directory.recordWithUID(u"__sagen1__")
+        group1 = yield self.directory.recordWithUID(u"__top_group_1__")
+        group2 = yield self.directory.recordWithUID(u"__sub_group_1__")
 
         # Add group delegate, but before the group membership has been
         # pulled in
         yield addDelegate(txn, delegator, group1, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
+        # Passing expanded=False will return the group
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=False))
+        self.assertEquals(1, len(delegates))
+        self.assertEquals(delegates[0].uid, u"__top_group_1__")
+        # Passing expanded=True will return not the group -- it only returns
+        # non-groups
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
         self.assertEquals(0, len(delegates))
 
         # Now refresh the group and there will be 3 delegates (contained
         # within 2 nested groups)
         # guid = "49b350c69611477b94d95516b13856ab"
-        yield self.groupCacher.refreshGroup(txn, group1.guid)
-        yield self.groupCacher.refreshGroup(txn, group2.guid)
-        delegates = (yield delegatesOf(txn, delegator, True))
+        yield self.groupCacher.refreshGroup(txn, group1.uid)
+        yield self.groupCacher.refreshGroup(txn, group2.uid)
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
         self.assertEquals(
-            set(["sagen", "cdaboo", "glyph"]),
-            set([d.shortNames[0] for d in delegates])
+            set([u"__sagen1__", u"__cdaboo1__", u"__glyph1__"]),
+            set([d.uid for d in delegates])
         )
         delegators = (yield delegatedTo(txn, delegate1, True))
-        self.assertEquals(["wsanchez"], [d.shortNames[0] for d in delegators])
+        self.assertEquals([u"__wsanchez1__"], [d.uid for d in delegators])
 
         # Verify we can ask for all delegated-to groups
         yield addDelegate(txn, delegator, group2, True)
-        groups = (yield allGroupDelegates(txn))
+        groups = (yield txn.allGroupDelegates())
         self.assertEquals(
-            set([
-                UUID("49b350c69611477b94d95516b13856ab"),
-                UUID("86144f73345a409782f1b782672087c7")
-                ]), set(groups))
+            set([u'__sub_group_1__', u'__top_group_1__']), set(groups)
+        )
 
         # Delegate to a user who is already indirectly delegated-to
         yield addDelegate(txn, delegator, delegate1, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
         self.assertEquals(
-            set(["sagen", "cdaboo", "glyph"]),
-            set([d.shortNames[0] for d in delegates])
+            set([u"__sagen1__", u"__cdaboo1__", u"__glyph1__"]),
+            set([d.uid for d in delegates])
         )
 
         # Add a member to the group; they become a delegate
         newSet = set()
-        for name in (u"wsanchez", u"cdaboo", u"sagen", u"glyph", u"dre"):
+        for name in (u"wsanchez1", u"cdaboo1", u"sagen1", u"glyph1", u"dre1"):
             record = (
-                yield self.xmlService.recordWithShortName(RecordType.user, name)
+                yield self.directory.recordWithShortName(RecordType.user, name)
             )
-            newSet.add(record.guid)
-        groupID, name, membershipHash = (yield txn.groupByGUID(group1.guid))
+            newSet.add(record.uid)
+        groupID, name, membershipHash = (yield txn.groupByUID(group1.uid))
         numAdded, numRemoved = (
             yield self.groupCacher.synchronizeMembers(txn, groupID, newSet)
         )
-        delegates = (yield delegatesOf(txn, delegator, True))
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
         self.assertEquals(
-            set(["sagen", "cdaboo", "glyph", "dre"]),
-            set([d.shortNames[0] for d in delegates])
+            set([u"__sagen1__", u"__cdaboo1__", u"__glyph1__", u"__dre1__"]),
+            set([d.uid for d in delegates])
         )
 
         # Remove delegate access from the top group
         yield removeDelegate(txn, delegator, group1, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
         self.assertEquals(
-            set(["sagen", "cdaboo"]),
-            set([d.shortNames[0] for d in delegates])
+            set([u"__sagen1__", u"__cdaboo1__"]),
+            set([d.uid for d in delegates])
         )
 
         # Remove delegate access from the sub group
         yield removeDelegate(txn, delegator, group2, True)
-        delegates = (yield delegatesOf(txn, delegator, True))
+        delegates = (yield delegatesOf(txn, delegator, True, expanded=True))
         self.assertEquals(
-            set(["sagen"]),
-            set([d.shortNames[0] for d in delegates])
+            set([u"__sagen1__"]),
+            set([d.uid for d in delegates])
         )
-
-
-
-testXMLConfig = """<?xml version="1.0" encoding="utf-8"?>
-
-<directory realm="xyzzy">
-
-  <record type="user">
-    <uid>__wsanchez__</uid>
-    <guid>3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F</guid>
-    <short-name>wsanchez</short-name>
-    <short-name>wilfredo_sanchez</short-name>
-    <full-name>Wilfredo Sanchez</full-name>
-    <password>zehcnasw</password>
-    <email>wsanchez at bitbucket.calendarserver.org</email>
-    <email>wsanchez at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__glyph__</uid>
-    <guid>9064DF91-1DBC-4E07-9C2B-6839B0953876</guid>
-    <short-name>glyph</short-name>
-    <full-name>Glyph Lefkowitz</full-name>
-    <password>hpylg</password>
-    <email>glyph at bitbucket.calendarserver.org</email>
-    <email>glyph at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__sagen__</uid>
-    <guid>4AD155CB-AE9B-475F-986C-E08A7537893E</guid>
-    <short-name>sagen</short-name>
-    <full-name>Morgen Sagen</full-name>
-    <password>negas</password>
-    <email>sagen at bitbucket.calendarserver.org</email>
-    <email>shared at example.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__cdaboo__</uid>
-    <guid>7D45CB10-479E-456B-B54D-528958C5734B</guid>
-    <short-name>cdaboo</short-name>
-    <full-name>Cyrus Daboo</full-name>
-    <password>suryc</password>
-    <email>cdaboo at bitbucket.calendarserver.org</email>
-  </record>
-
-  <record type="user">
-    <uid>__dre__</uid>
-    <guid>CFC88493-DBFF-42B9-ADC7-9B3DA0B0769B</guid>
-    <short-name>dre</short-name>
-    <full-name>Andre LaBranche</full-name>
-    <password>erd</password>
-    <email>dre at bitbucket.calendarserver.org</email>
-    <email>shared at example.com</email>
-  </record>
-
-  <record type="group">
-    <uid>__top_group_1__</uid>
-    <guid>49B350C6-9611-477B-94D9-5516B13856AB</guid>
-    <short-name>top-group-1</short-name>
-    <full-name>Top Group 1</full-name>
-    <email>topgroup1 at example.com</email>
-    <member-uid>__wsanchez__</member-uid>
-    <member-uid>__glyph__</member-uid>
-    <member-uid>__sub_group_1__</member-uid>
-  </record>
-
-  <record type="group">
-    <uid>__sub_group_1__</uid>
-    <guid>86144F73-345A-4097-82F1-B782672087C7</guid>
-    <short-name>sub-group-1</short-name>
-    <full-name>Sub Group 1</full-name>
-    <email>subgroup1 at example.com</email>
-    <member-uid>__sagen__</member-uid>
-    <member-uid>__cdaboo__</member-uid>
-  </record>
-
-</directory>
-"""
+        yield txn.commit()

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_directory.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/test/test_directory.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_directory.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_directory.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,125 @@
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+Directory tests
+"""
+
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.test.util import StoreTestCase
+from twext.who.directory import DirectoryRecord
+from twext.who.idirectory import FieldName, RecordType
+from txdav.who.directory import CalendarDirectoryRecordMixin
+from uuid import UUID
+from twext.who.expression import (
+    MatchType, MatchFlags, MatchExpression
+)
+
+
+
+class TestDirectoryRecord(DirectoryRecord, CalendarDirectoryRecordMixin):
+    pass
+
+
+class DirectoryTestCase(StoreTestCase):
+
+    @inlineCallbacks
+    def test_expandedMembers(self):
+
+        record = yield self.directory.recordWithUID(u"both_coasts")
+
+        direct = yield record.members()
+        self.assertEquals(
+            set([u"left_coast", u"right_coast"]),
+            set([r.uid for r in direct])
+        )
+
+        expanded = yield record.expandedMembers()
+        self.assertEquals(
+            set([u"Chris Lecroy", u"Cyrus Daboo", u"David Reid", u"Wilfredo Sanchez"]),
+            set([r.displayName for r in expanded])
+        )
+
+
+    def test_canonicalCalendarUserAddress(self):
+
+        record = TestDirectoryRecord(
+            self.directory,
+            {
+                FieldName.uid: u"uid",
+                FieldName.shortNames: [u"name"],
+                FieldName.recordType: RecordType.user,
+            }
+        )
+        self.assertEquals(
+            record.canonicalCalendarUserAddress(),
+            u"/principals/__uids__/uid/"
+        )
+
+
+        record = TestDirectoryRecord(
+            self.directory,
+            {
+                FieldName.uid: u"uid",
+                FieldName.shortNames: [u"name"],
+                FieldName.emailAddresses: [u"test at example.com"],
+                FieldName.recordType: RecordType.user,
+            }
+        )
+        self.assertEquals(
+            record.canonicalCalendarUserAddress(),
+            u"mailto:test at example.com"
+        )
+
+
+        record = TestDirectoryRecord(
+            self.directory,
+            {
+                FieldName.uid: u"uid",
+                FieldName.guid: UUID("E2F6C57F-BB15-4EF9-B0AC-47A7578386F1"),
+                FieldName.shortNames: [u"name"],
+                FieldName.emailAddresses: [u"test at example.com"],
+                FieldName.recordType: RecordType.user,
+            }
+        )
+        self.assertEquals(
+            record.canonicalCalendarUserAddress(),
+            u"urn:uuid:E2F6C57F-BB15-4EF9-B0AC-47A7578386F1"
+        )
+
+
+    @inlineCallbacks
+    def test_recordsFromMatchExpression(self):
+        expression = MatchExpression(
+            FieldName.uid,
+            u"6423F94A-6B76-4A3A-815B-D52CFD77935D",
+            MatchType.equals,
+            MatchFlags.none
+        )
+        records = yield self.directory.recordsFromExpression(expression)
+        self.assertEquals(len(records), 1)
+
+
+    @inlineCallbacks
+    def test_recordsFromMatchExpressionNonUnicode(self):
+        expression = MatchExpression(
+            FieldName.guid,
+            UUID("6423F94A-6B76-4A3A-815B-D52CFD77935D"),
+            MatchType.equals,
+            MatchFlags.caseInsensitive
+        )
+        records = yield self.directory.recordsFromExpression(expression)
+        self.assertEquals(len(records), 1)

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_groups.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_groups.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_groups.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -18,13 +18,11 @@
 Group membership caching implementation tests
 """
 
-from txdav.who.groups import GroupCacher, expandedMembers, diffAssignments
+from txdav.who.groups import GroupCacher, diffAssignments
 from twext.who.idirectory import RecordType
-from twext.who.test.test_xml import xmlService
 from twisted.internet.defer import inlineCallbacks
 from twistedcaldav.test.util import StoreTestCase
 from txdav.common.icommondatastore import NotFoundError
-from uuid import UUID
 
 
 class GroupCacherTest(StoreTestCase):
@@ -32,33 +30,10 @@
     @inlineCallbacks
     def setUp(self):
         yield super(GroupCacherTest, self).setUp()
-        self.xmlService = xmlService(self.mktemp(), xmlData=testXMLConfig)
-        self.groupCacher = GroupCacher(self.xmlService)
+        self.groupCacher = GroupCacher(self.directory)
 
 
     @inlineCallbacks
-    def test_expandedMembers(self):
-        """
-        Verify expandedMembers() returns a "flattened" set of records
-        belonging to a group (and does not return sub-groups themselves,
-        only their members)
-        """
-        record = yield self.xmlService.recordWithUID(u"__top_group_1__")
-        memberUIDs = set()
-        for member in (yield expandedMembers(record)):
-            memberUIDs.add(member.uid)
-        self.assertEquals(
-            memberUIDs,
-            set(["__cdaboo__", "__glyph__", "__sagen__", "__wsanchez__"])
-        )
-
-        # Non group records return an empty set() of members
-        record = yield self.xmlService.recordWithUID(u"__sagen__")
-        members = yield expandedMembers(record)
-        self.assertEquals(0, len(list(members)))
-
-
-    @inlineCallbacks
     def test_refreshGroup(self):
         """
         Verify refreshGroup() adds a group to the Groups table with the
@@ -68,37 +43,35 @@
         store = self.storeUnderTest()
         txn = store.newTransaction()
 
-        record = yield self.xmlService.recordWithUID(u"__top_group_1__")
-        yield self.groupCacher.refreshGroup(txn, record.guid)
+        record = yield self.directory.recordWithUID(u"__top_group_1__")
+        yield self.groupCacher.refreshGroup(txn, record.uid)
 
-        groupID, name, membershipHash = (yield txn.groupByGUID(record.guid))
-        self.assertEquals(membershipHash, "4b0e162f2937f0f3daa6d10e5a6a6c33")
+        groupID, name, membershipHash = (yield txn.groupByUID(record.uid))
 
-        groupGUID, name, membershipHash = (yield txn.groupByID(groupID))
-        self.assertEquals(groupGUID, record.guid)
-        self.assertEquals(name, "Top Group 1")
-        self.assertEquals(membershipHash, "4b0e162f2937f0f3daa6d10e5a6a6c33")
+        self.assertEquals(membershipHash, "553eb54e3bbb26582198ee04541dbee4")
 
+        groupUID, name, membershipHash = (yield txn.groupByID(groupID))
+        self.assertEquals(groupUID, record.uid)
+        self.assertEquals(name, u"Top Group 1")
+        self.assertEquals(membershipHash, "553eb54e3bbb26582198ee04541dbee4")
+
         members = (yield txn.membersOfGroup(groupID))
         self.assertEquals(
-            set([UUID("9064df911dbc4e079c2b6839b0953876"),
-                 UUID("4ad155cbae9b475f986ce08a7537893e"),
-                 UUID("3bdcb95484d54f6d8035eac19a6d6e1f"),
-                 UUID("7d45cb10479e456bb54d528958c5734b")]),
+            set([u'__cdaboo1__', u'__glyph1__', u'__sagen1__', u'__wsanchez1__']),
             members
         )
 
         records = (yield self.groupCacher.cachedMembers(txn, groupID))
         self.assertEquals(
-            set([r.shortNames[0] for r in records]),
-            set(["wsanchez", "cdaboo", "glyph", "sagen"])
+            set([r.uid for r in records]),
+            set([u'__cdaboo1__', u'__glyph1__', u'__sagen1__', u'__wsanchez1__'])
         )
 
         # sagen is in the top group, even though it's actually one level
         # removed
-        record = yield self.xmlService.recordWithUID(u"__sagen__")
-        groups = (yield self.groupCacher.cachedGroupsFor(txn, record.guid))
-        self.assertEquals(set([groupID]), groups)
+        record = yield self.directory.recordWithUID(u"__sagen1__")
+        groups = (yield self.groupCacher.cachedGroupsFor(txn, record.uid))
+        self.assertEquals(set([u"__top_group_1__"]), groups)
 
 
     @inlineCallbacks
@@ -113,20 +86,20 @@
         txn = store.newTransaction()
 
         # Refresh the group so it's assigned a group_id
-        guid = UUID("49b350c69611477b94d95516b13856ab")
-        yield self.groupCacher.refreshGroup(txn, guid)
-        groupID, name, membershipHash = (yield txn.groupByGUID(guid))
+        uid = u"__top_group_1__"
+        yield self.groupCacher.refreshGroup(txn, uid)
+        groupID, name, membershipHash = (yield txn.groupByUID(uid))
 
         # Remove two members, and add one member
         newSet = set()
-        for name in (u"wsanchez", u"cdaboo", u"dre"):
+        for name in (u"wsanchez1", u"cdaboo1", u"dre1"):
             record = (
-                yield self.xmlService.recordWithShortName(
+                yield self.directory.recordWithShortName(
                     RecordType.user,
                     name
                 )
             )
-            newSet.add(record.guid)
+            newSet.add(record.uid)
         numAdded, numRemoved = (
             yield self.groupCacher.synchronizeMembers(
                 txn, groupID, newSet
@@ -137,7 +110,7 @@
         records = (yield self.groupCacher.cachedMembers(txn, groupID))
         self.assertEquals(
             set([r.shortNames[0] for r in records]),
-            set(["wsanchez", "cdaboo", "dre"])
+            set(["wsanchez1", "cdaboo1", "dre1"])
         )
 
         # Remove all members
@@ -159,12 +132,12 @@
         # Non-existent groupID
         self.failUnlessFailure(txn.groupByID(42), NotFoundError)
 
-        guid = UUID("49b350c69611477b94d95516b13856ab")
-        hash = "4b0e162f2937f0f3daa6d10e5a6a6c33"
-        yield self.groupCacher.refreshGroup(txn, guid)
-        groupID, name, membershipHash = (yield txn.groupByGUID(guid))
+        uid = u"__top_group_1__"
+        hash = "553eb54e3bbb26582198ee04541dbee4"
+        yield self.groupCacher.refreshGroup(txn, uid)
+        groupID, name, membershipHash = (yield txn.groupByUID(uid))
         results = (yield txn.groupByID(groupID))
-        self.assertEquals([guid, "Top Group 1", hash], results)
+        self.assertEquals((uid, u"Top Group 1", hash), results)
 
 
     @inlineCallbacks
@@ -177,32 +150,31 @@
         self.assertEquals(oldExternalAssignments, {})
 
         newAssignments = {
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"):
-            (None, UUID("49B350C6-9611-477B-94D9-5516B13856AB"))
+            u"__wsanchez1__": (None, u"__top_group_1__")
         }
         yield self.groupCacher.applyExternalAssignments(txn, newAssignments)
         oldExternalAssignments = (yield txn.externalDelegates())
         self.assertEquals(
             oldExternalAssignments,
             {
-                UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"):
+                u"__wsanchez1__":
                 (
                     None,
-                    UUID("49B350C6-9611-477B-94D9-5516B13856AB")
+                    u"__top_group_1__"
                 )
             }
         )
 
         newAssignments = {
-            UUID("7D45CB10-479E-456B-B54D-528958C5734B"):
+            u"__cdaboo1__":
             (
-                UUID("86144F73-345A-4097-82F1-B782672087C7"),
+                u"__sub_group_1__",
                 None
             ),
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"):
+            u"__wsanchez1__":
             (
-                UUID("86144F73-345A-4097-82F1-B782672087C7"),
-                UUID("49B350C6-9611-477B-94D9-5516B13856AB")
+                u"__sub_group_1__",
+                u"__top_group_1__"
             ),
         }
         yield self.groupCacher.applyExternalAssignments(txn, newAssignments)
@@ -210,14 +182,14 @@
         self.assertEquals(
             oldExternalAssignments,
             {
-                UUID('3bdcb954-84d5-4f6d-8035-eac19a6d6e1f'):
+                u"__wsanchez1__":
                 (
-                    UUID('86144f73-345a-4097-82f1-b782672087c7'),
-                    UUID('49b350c6-9611-477b-94d9-5516b13856ab')
+                    u"__sub_group_1__",
+                    u"__top_group_1__"
                 ),
-                UUID('7d45cb10-479e-456b-b54d-528958c5734b'):
+                u"__cdaboo1__":
                 (
-                    UUID('86144f73-345a-4097-82f1-b782672087c7'),
+                    u"__sub_group_1__",
                     None
                 )
             }
@@ -228,44 +200,44 @@
             allGroupDelegates,
             set(
                 [
-                    UUID('49b350c6-9611-477b-94d9-5516b13856ab'),
-                    UUID('86144f73-345a-4097-82f1-b782672087c7')
+                    u"__top_group_1__",
+                    u"__sub_group_1__"
                 ]
             )
         )
 
         # Fault in the read-only group
-        yield self.groupCacher.refreshGroup(txn, UUID('86144f73-345a-4097-82f1-b782672087c7'))
+        yield self.groupCacher.refreshGroup(txn, u"__sub_group_1__")
 
         # Wilfredo should have Sagen and Daboo as read-only delegates
         delegates = (yield txn.delegates(
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"), False)
+            u"__wsanchez1__", False, expanded=True)
         )
         self.assertEquals(
             delegates,
             set(
                 [
-                    UUID('4ad155cb-ae9b-475f-986c-e08a7537893e'),
-                    UUID('7d45cb10-479e-456b-b54d-528958c5734b')
+                    u"__sagen1__",
+                    u"__cdaboo1__"
                 ]
             )
         )
 
         # Fault in the read-write group
-        yield self.groupCacher.refreshGroup(txn, UUID('49b350c6-9611-477b-94d9-5516b13856ab'))
+        yield self.groupCacher.refreshGroup(txn, u"__top_group_1__")
 
         # Wilfredo should have 4 users as read-write delegates
         delegates = (yield txn.delegates(
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"), True)
+            u"__wsanchez1__", True, expanded=True)
         )
         self.assertEquals(
             delegates,
             set(
                 [
-                    UUID('3bdcb954-84d5-4f6d-8035-eac19a6d6e1f'),
-                    UUID('4ad155cb-ae9b-475f-986c-e08a7537893e'),
-                    UUID('7d45cb10-479e-456b-b54d-528958c5734b'),
-                    UUID('9064df91-1dbc-4e07-9c2b-6839b0953876')
+                    u"__wsanchez1__",
+                    u"__sagen1__",
+                    u"__cdaboo1__",
+                    u"__glyph1__"
                 ]
             )
         )
@@ -275,9 +247,9 @@
         # Now, remove some external assignments
         #
         newAssignments = {
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"):
+            u"__wsanchez1__":
             (
-                UUID("86144F73-345A-4097-82F1-B782672087C7"),
+                u"__sub_group_1__",
                 None
             ),
         }
@@ -286,9 +258,9 @@
         self.assertEquals(
             oldExternalAssignments,
             {
-                UUID('3bdcb954-84d5-4f6d-8035-eac19a6d6e1f'):
+                u"__wsanchez1__":
                 (
-                    UUID('86144f73-345a-4097-82f1-b782672087c7'),
+                    u"__sub_group_1__",
                     None
                 ),
             }
@@ -299,28 +271,28 @@
             allGroupDelegates,
             set(
                 [
-                    UUID('86144f73-345a-4097-82f1-b782672087c7')
+                    u"__sub_group_1__"
                 ]
             )
         )
 
         # Wilfredo should have Sagen and Daboo as read-only delegates
         delegates = (yield txn.delegates(
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"), False)
+            u"__wsanchez1__", False, expanded=True)
         )
         self.assertEquals(
             delegates,
             set(
                 [
-                    UUID('4ad155cb-ae9b-475f-986c-e08a7537893e'),
-                    UUID('7d45cb10-479e-456b-b54d-528958c5734b')
+                    u"__sagen1__",
+                    u"__cdaboo1__"
                 ]
             )
         )
 
         # Wilfredo should have no read-write delegates
         delegates = (yield txn.delegates(
-            UUID("3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F"), True)
+            u"__wsanchez1__", True, expanded=True)
         )
         self.assertEquals(
             delegates,
@@ -333,7 +305,7 @@
             allGroupDelegates,
             set(
                 [
-                    UUID('86144f73-345a-4097-82f1-b782672087c7')
+                    u"__sub_group_1__"
                 ]
             )
         )
@@ -417,81 +389,3 @@
                 {"D": ("7", "8"), "C": ("4", "5"), "A": ("1", "2")},
             )
         )
-
-testXMLConfig = """<?xml version="1.0" encoding="utf-8"?>
-
-<directory realm="xyzzy">
-
-  <record type="user">
-    <uid>__wsanchez__</uid>
-    <guid>3BDCB954-84D5-4F6D-8035-EAC19A6D6E1F</guid>
-    <short-name>wsanchez</short-name>
-    <short-name>wilfredo_sanchez</short-name>
-    <full-name>Wilfredo Sanchez</full-name>
-    <password>zehcnasw</password>
-    <email>wsanchez at bitbucket.calendarserver.org</email>
-    <email>wsanchez at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__glyph__</uid>
-    <guid>9064DF91-1DBC-4E07-9C2B-6839B0953876</guid>
-    <short-name>glyph</short-name>
-    <full-name>Glyph Lefkowitz</full-name>
-    <password>hpylg</password>
-    <email>glyph at bitbucket.calendarserver.org</email>
-    <email>glyph at devnull.twistedmatrix.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__sagen__</uid>
-    <guid>4AD155CB-AE9B-475F-986C-E08A7537893E</guid>
-    <short-name>sagen</short-name>
-    <full-name>Morgen Sagen</full-name>
-    <password>negas</password>
-    <email>sagen at bitbucket.calendarserver.org</email>
-    <email>shared at example.com</email>
-  </record>
-
-  <record type="user">
-    <uid>__cdaboo__</uid>
-    <guid>7D45CB10-479E-456B-B54D-528958C5734B</guid>
-    <short-name>cdaboo</short-name>
-    <full-name>Cyrus Daboo</full-name>
-    <password>suryc</password>
-    <email>cdaboo at bitbucket.calendarserver.org</email>
-  </record>
-
-  <record type="user">
-    <uid>__dre__</uid>
-    <guid>CFC88493-DBFF-42B9-ADC7-9B3DA0B0769B</guid>
-    <short-name>dre</short-name>
-    <full-name>Andre LaBranche</full-name>
-    <password>erd</password>
-    <email>dre at bitbucket.calendarserver.org</email>
-    <email>shared at example.com</email>
-  </record>
-
-  <record type="group">
-    <uid>__top_group_1__</uid>
-    <guid>49B350C6-9611-477B-94D9-5516B13856AB</guid>
-    <short-name>top-group-1</short-name>
-    <full-name>Top Group 1</full-name>
-    <email>topgroup1 at example.com</email>
-    <member-uid>__wsanchez__</member-uid>
-    <member-uid>__glyph__</member-uid>
-    <member-uid>__sub_group_1__</member-uid>
-  </record>
-
-  <record type="group">
-    <uid>__sub_group_1__</uid>
-    <guid>86144F73-345A-4097-82F1-B782672087C7</guid>
-    <short-name>sub-group-1</short-name>
-    <full-name>Sub Group 1</full-name>
-    <email>subgroup1 at example.com</email>
-    <member-uid>__sagen__</member-uid>
-    <member-uid>__cdaboo__</member-uid>
-  </record>
-
-</directory>
-"""

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_util.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/test/test_util.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_util.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,164 @@
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+txdav.who.util tests
+"""
+
+import os
+
+from txdav.who.util import directoryFromConfig
+from twisted.internet.defer import inlineCallbacks
+from twisted.trial.unittest import TestCase
+from twistedcaldav.config import ConfigDict
+from twisted.python.filepath import FilePath
+from txdav.who.augment import AugmentedDirectoryService
+from twext.who.aggregate import DirectoryService as AggregateDirectoryService
+from twext.who.xml import DirectoryService as XMLDirectoryService
+from txdav.who.delegates import (
+    DirectoryService as DelegateDirectoryService,
+    RecordType as DelegateRecordType
+)
+from twext.who.idirectory import RecordType
+from txdav.who.idirectory import RecordType as CalRecordType
+from txdav.who.wiki import (
+    DirectoryService as WikiDirectoryService,
+    RecordType as WikiRecordType,
+)
+
+
+class StubStore(object):
+    pass
+
+
+
+class UtilTest(TestCase):
+
+    def setUp(self):
+        sourceDir = FilePath(__file__).parent().child("accounts")
+        self.serverRoot = os.path.abspath(self.mktemp())
+        os.mkdir(self.serverRoot)
+        self.dataRoot = os.path.join(self.serverRoot, "data")
+        if not os.path.exists(self.dataRoot):
+            os.makedirs(self.dataRoot)
+        destDir = FilePath(self.dataRoot)
+
+        accounts = destDir.child("accounts.xml")
+        sourceAccounts = sourceDir.child("accounts.xml")
+        accounts.setContent(sourceAccounts.getContent())
+
+        resources = destDir.child("resources.xml")
+        sourceResources = sourceDir.child("resources.xml")
+        resources.setContent(sourceResources.getContent())
+
+        augments = destDir.child("augments.xml")
+        sourceAugments = sourceDir.child("augments.xml")
+        augments.setContent(sourceAugments.getContent())
+
+
+    @inlineCallbacks
+    def test_directoryFromConfig(self):
+
+        config = ConfigDict(
+            {
+                "DataRoot": self.dataRoot,
+                "Authentication": {
+                    "Wiki": {
+                        "Enabled": True,
+                        "CollabHost": "localhost",
+                        "CollabPort": 4444,
+                    },
+                },
+                "DirectoryService": {
+                    "Enabled": True,
+                    "type": "XML",
+                    "params": {
+                        "xmlFile": "accounts.xml",
+                        "recordTypes": ["users", "groups"],
+                    },
+                },
+                "ResourceService": {
+                    "Enabled": True,
+                    "type": "XML",
+                    "params": {
+                        "xmlFile": "resources.xml",
+                        "recordTypes": ["locations", "resources", "addresses"],
+                    },
+                },
+                "AugmentService": {
+                    "Enabled": True,
+                    # FIXME: This still uses an actual class name:
+                    "type": "twistedcaldav.directory.augment.AugmentXMLDB",
+                    "params": {
+                        "xmlFiles": ["augments.xml"],
+                    },
+                },
+            }
+        )
+
+        store = StubStore()
+        service = directoryFromConfig(config, store=store)
+
+        # Inspect the directory service structure
+        self.assertTrue(isinstance(service, AugmentedDirectoryService))
+        self.assertTrue(isinstance(service._directory, AggregateDirectoryService))
+        self.assertEquals(len(service._directory.services), 4)
+        self.assertTrue(
+            isinstance(service._directory.services[0], XMLDirectoryService)
+        )
+        self.assertEquals(
+            set(service._directory.services[0].recordTypes()),
+            set([RecordType.user, RecordType.group])
+        )
+        self.assertTrue(
+            isinstance(service._directory.services[1], XMLDirectoryService)
+        )
+        self.assertEquals(
+            set(service._directory.services[1].recordTypes()),
+            set(
+                [
+                    CalRecordType.location,
+                    CalRecordType.resource,
+                    CalRecordType.address
+                ]
+            )
+        )
+        self.assertTrue(
+            isinstance(service._directory.services[2], DelegateDirectoryService)
+        )
+        self.assertEquals(
+            set(service._directory.services[2].recordTypes()),
+            set(
+                [
+                    DelegateRecordType.readDelegateGroup,
+                    DelegateRecordType.writeDelegateGroup,
+                    DelegateRecordType.readDelegatorGroup,
+                    DelegateRecordType.writeDelegatorGroup,
+                ]
+            )
+        )
+        self.assertTrue(
+            isinstance(service._directory.services[3], WikiDirectoryService)
+        )
+        self.assertEquals(
+            set(service._directory.services[3].recordTypes()),
+            set([WikiRecordType.macOSXServerWiki])
+        )
+
+
+        # And make sure it's functional:
+        record = yield service.recordWithUID("group07")
+        self.assertEquals(record.fullNames, [u'Group 07'])

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_wiki.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/test/test_wiki.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_wiki.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/test/test_wiki.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,116 @@
+##
+# Copyright (c) 2014 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 print_function
+from __future__ import absolute_import
+
+"""
+Tests for L{txdav.who.wiki}.
+"""
+
+
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks, succeed
+from twistedcaldav.test.util import StoreTestCase
+
+from ..wiki import DirectoryService, WikiAccessLevel
+import txdav.who.wiki
+
+
+class WikiIndividualServiceTestCase(unittest.TestCase):
+    """
+    Instantiate a wiki service directly
+    """
+
+    @inlineCallbacks
+    def test_service(self):
+        service = DirectoryService("realm", "localhost", 4444)
+        record = yield service.recordWithUID(u"[wiki]test")
+        self.assertEquals(
+            record.shortNames[0],
+            u"test"
+        )
+
+
+
+class WikiAggregateServiceTestCase(StoreTestCase):
+    """
+    Get a wiki service as part of directoryFromConfig
+    """
+
+    def configure(self):
+        """
+        Override configuration hook to turn on wiki service.
+        """
+        from twistedcaldav.config import config
+
+        super(WikiAggregateServiceTestCase, self).configure()
+        self.patch(config.Authentication.Wiki, "Enabled", True)
+
+
+    @inlineCallbacks
+    def test_service(self):
+        record = yield self.directory.recordWithUID(u"[wiki]test")
+        self.assertEquals(
+            record.shortNames[0],
+            u"test"
+        )
+
+
+
+class AccessForRecordTestCase(StoreTestCase):
+    """
+    Exercise accessForRecord
+    """
+
+    def configure(self):
+        """
+        Override configuration hook to turn on wiki service.
+        """
+        from twistedcaldav.config import config
+
+        super(AccessForRecordTestCase, self).configure()
+        self.patch(config.Authentication.Wiki, "Enabled", True)
+        self.patch(
+            txdav.who.wiki,
+            "accessForUserToWiki",
+            self.stubAccessForUserToWiki
+        )
+
+
+    def stubAccessForUserToWiki(self, *args, **kwds):
+        return succeed(self.access)
+
+
+    @inlineCallbacks
+    def test_accessForRecord(self):
+        record = yield self.directory.recordWithUID(u"[wiki]test")
+
+        self.access = "no-access"
+        access = yield record.accessForRecord(None)
+        self.assertEquals(access, WikiAccessLevel.none)
+
+        self.access = "read"
+        access = yield record.accessForRecord(None)
+        self.assertEquals(access, WikiAccessLevel.read)
+
+        self.access = "write"
+        access = yield record.accessForRecord(None)
+        self.assertEquals(access, WikiAccessLevel.write)
+
+        self.access = "admin"
+        access = yield record.accessForRecord(None)
+        self.assertEquals(access, WikiAccessLevel.write)

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/util.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/util.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/util.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,203 @@
+##
+# Copyright (c) 2006-2014 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 twext.python.log import Logger
+from twisted.cred.credentials import UsernamePassword
+from twext.who.aggregate import DirectoryService as AggregateDirectoryService
+from txdav.who.augment import AugmentedDirectoryService
+
+from calendarserver.tap.util import getDBPool, storeFromConfig
+from twext.who.idirectory import (
+    RecordType, DirectoryConfigurationError
+)
+from twext.who.ldap import DirectoryService as LDAPDirectoryService
+from twext.who.util import ConstantsContainer
+from twisted.python.filepath import FilePath
+from twisted.python.reflect import namedClass
+from twistedcaldav.config import fullServerPath
+from txdav.who.delegates import DirectoryService as DelegateDirectoryService
+from txdav.who.idirectory import (
+    RecordType as CalRecordType,
+    FieldName as CalFieldName
+)
+from txdav.who.xml import DirectoryService as XMLDirectoryService
+from txdav.who.wiki import DirectoryService as WikiDirectoryService
+
+
+log = Logger()
+
+
+def directoryFromConfig(config, store=None):
+    """
+    Return a directory service based on the config.  If you want to go through
+    AMP to talk to one of these as a client, instantiate
+    txdav.dps.client.DirectoryService
+    """
+
+    # MOVE2WHO FIXME: this needs to talk to its own separate database.  In
+    # fact, don't pass store=None if you already have called storeFromConfig()
+    # within this process.  Pass the existing store in here.
+
+    # TODO: use proxyForInterface to ensure we're only using the DPS related
+    # store API.  Also define an IDirectoryProxyStore Interface
+    if store is None:
+        pool, txnFactory = getDBPool(config)
+        store = storeFromConfig(config, txnFactory, None)
+
+    aggregatedServices = []
+
+
+    for serviceKey in ("DirectoryService", "ResourceService"):
+        serviceValue = config.get(serviceKey, None)
+
+        if not serviceValue.Enabled:
+            continue
+
+        directoryType = serviceValue.type.lower()
+        params = serviceValue.params
+
+        # TODO: add a "test" directory service that produces test records
+        # from code -- no files needed.
+
+        if "xml" in directoryType:
+            xmlFile = params.xmlFile
+            xmlFile = fullServerPath(config.DataRoot, xmlFile)
+            if not xmlFile or not os.path.exists(xmlFile):
+                log.error("Path not found for XML directory: {p}", p=xmlFile)
+            fp = FilePath(xmlFile)
+            directory = XMLDirectoryService(fp)
+
+        elif "opendirectory" in directoryType:
+            from twext.who.opendirectory import (
+                DirectoryService as ODDirectoryService
+            )
+            directory = ODDirectoryService()
+
+        elif "ldap" in directoryType:
+            if params.credentials.dn and params.credentials.password:
+                creds = UsernamePassword(
+                    params.credentials.dn,
+                    params.credentials.password
+                )
+            else:
+                creds = None
+            directory = LDAPDirectoryService(
+                params.uri,
+                params.rdnSchema.base,
+                creds=creds
+            )
+
+        else:
+            log.error("Invalid DirectoryType: {dt}", dt=directoryType)
+            raise DirectoryConfigurationError
+
+        # Set the appropriate record types on each service
+        types = []
+        fieldNames = []
+        for recordTypeName in params.recordTypes:
+            recordType = {
+                "users": RecordType.user,
+                "groups": RecordType.group,
+                "locations": CalRecordType.location,
+                "resources": CalRecordType.resource,
+                "addresses": CalRecordType.address,
+            }.get(recordTypeName, None)
+
+            if recordType is None:
+                log.error("Invalid Record Type: {rt}", rt=recordTypeName)
+                raise DirectoryConfigurationError
+
+            if recordType in types:
+                log.error("Duplicate Record Type: {rt}", rt=recordTypeName)
+                raise DirectoryConfigurationError
+
+            types.append(recordType)
+
+        directory.recordType = ConstantsContainer(types)
+        directory.fieldName = ConstantsContainer(
+            (directory.fieldName, CalFieldName)
+        )
+        fieldNames.append(directory.fieldName)
+        aggregatedServices.append(directory)
+
+    #
+    # Setup the Augment Service
+    #
+    if config.AugmentService.type:
+        augmentClass = namedClass(config.AugmentService.type)
+        log.info(
+            "Configuring augment service of type: {augmentClass}",
+            augmentClass=augmentClass
+        )
+        try:
+            augmentService = augmentClass(**config.AugmentService.params)
+        except IOError:
+            log.error("Could not start augment service")
+            raise
+    else:
+        augmentService = None
+
+    userDirectory = None
+    for directory in aggregatedServices:
+        if RecordType.user in directory.recordTypes():
+            userDirectory = directory
+            break
+    else:
+        log.error("No directory service set up for users")
+        raise DirectoryConfigurationError
+
+    # Delegate service
+    delegateDirectory = DelegateDirectoryService(
+        userDirectory.realmName,
+        store
+    )
+    aggregatedServices.append(delegateDirectory)
+
+    # Wiki service
+    if config.Authentication.Wiki.Enabled:
+        aggregatedServices.append(
+            WikiDirectoryService(
+                userDirectory.realmName,
+                config.Authentication.Wiki.CollabHost,
+                config.Authentication.Wiki.CollabPort
+            )
+        )
+
+    # Aggregate service
+    aggregateDirectory = AggregateDirectoryService(
+        userDirectory.realmName, aggregatedServices
+    )
+
+    # Augment service
+    try:
+        fieldNames.append(CalFieldName)
+        augmented = AugmentedDirectoryService(
+            aggregateDirectory, store, augmentService
+        )
+        augmented.fieldName = ConstantsContainer(fieldNames)
+
+        # The delegate directory needs a way to look up user/group records
+        # so hand it a reference to the augmented directory.
+        # FIXME: is there a better pattern to use here?
+        delegateDirectory.setMasterDirectory(augmented)
+
+    except Exception as e:
+        log.error("Could not create directory service", error=e)
+        raise
+
+    return augmented

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/vcard.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/vcard.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/vcard.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/vcard.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,330 @@
+##
+# Copyright (c) 2006-2014 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.
+##
+
+"""
+    Utilities to converting a Record to a vCard
+"""
+
+__all__ = [
+    "vCardFromRecord"
+]
+
+from pycalendar.vcard.adr import Adr
+from pycalendar.vcard.n import N
+from twext.python.log import Logger
+from twext.who.idirectory import FieldName, RecordType
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twistedcaldav.config import config
+from twistedcaldav.vcard import Component, Property, vCardProductID
+from txdav.who.idirectory import FieldName as CalFieldName, \
+    RecordType as CalRecordType
+from txweb2.dav.util import joinURL
+
+log = Logger()
+
+
+recordTypeToVCardKindMap = {
+   RecordType.user: "individual",
+   RecordType.group: "group",
+   CalRecordType.location: "location",
+   CalRecordType.resource: "device",
+}
+
+vCardKindToRecordTypeMap = {
+   "individual" : RecordType.user,
+   "group": RecordType.group,
+   "org": RecordType.group,
+   "location": CalRecordType.location,
+   "device": CalRecordType.resource,
+}
+
+
+# all possible generated parameters.
+vCardPropToParamMap = {
+    #"PHOTO": {"ENCODING": ("B",), "TYPE": ("JPEG",), },
+    "ADR": {"TYPE": ("WORK", "PREF", "POSTAL", "PARCEL",), },
+    #"LABEL": {"TYPE": ("POSTAL", "PARCEL",)},
+    #"TEL": {"TYPE": None, },  # None means param can contain can be anything
+    "EMAIL": {"TYPE": None, },
+    #"KEY": {"ENCODING": ("B",), "TYPE": ("PGPPUBILICKEY", "USERCERTIFICATE", "USERPKCS12DATA", "USERSMIMECERTIFICATE",)},
+    #"URL": {"TYPE": ("WEBLOG", "HOMEPAGE",)},
+    #"IMPP": {"TYPE": ("PREF",), "X-SERVICE-TYPE": None, },
+    #"X-ABRELATEDNAMES": {"TYPE": None, },
+    #"X-AIM": {"TYPE": ("PREF",), },
+    #"X-JABBER": {"TYPE": ("PREF",), },
+    #"X-MSN": {"TYPE": ("PREF",), },
+    #"X-ICQ": {"TYPE": ("PREF",), },
+}
+
+
+vCardConstantProperties = {
+    #===================================================================
+    # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+    #===================================================================
+    # 3.6.3 PRODID
+    "PRODID": vCardProductID,
+    # 3.6.9 VERSION
+    "VERSION": "3.0",
+}
+
+
+ at inlineCallbacks
+def vCardFromRecord(record, forceKind=None, addProps=None, parentURI=None):
+
+    def isUniqueProperty(newProperty, ignoredParameters={}):
+        existingProperties = vcard.properties(newProperty.name())
+        for existingProperty in existingProperties:
+            if ignoredParameters:
+                existingProperty = existingProperty.duplicate()
+                for paramName, paramValues in ignoredParameters.iteritems():
+                    for paramValue in paramValues:
+                        existingProperty.removeParameterValue(paramName, paramValue)
+            if existingProperty == newProperty:
+                return False
+        return True
+
+
+    def addUniqueProperty(newProperty, ignoredParameters=None):
+        if isUniqueProperty(newProperty, ignoredParameters):
+            vcard.addProperty(newProperty)
+        else:
+            log.info(
+                "Ignoring property {prop!r} it is a duplicate",
+                prop=newProperty
+            )
+
+    #=======================================================================
+    # start
+    #=======================================================================
+
+    log.debug("vCardFromRecord: record={record}, forceKind={forceKind}, addProps={addProps}, parentURI={parentURI}",
+                   record=record, forceKind=forceKind, addProps=addProps, parentURI=parentURI)
+
+    if forceKind is None:
+        kind = recordTypeToVCardKindMap.get(record.recordType, "individual")
+    else:
+        kind = forceKind
+
+    constantProperties = vCardConstantProperties.copy()
+    if addProps:
+        for key, value in addProps.iteritems():
+            if key not in constantProperties:
+                constantProperties[key] = value
+
+    # create vCard
+    vcard = Component("VCARD")
+
+    # add constant properties
+    for key, value in constantProperties.items():
+        vcard.addProperty(Property(key, value))
+
+    #===========================================================================
+    # 2.1 Predefined Type Usage
+    #===========================================================================
+    # 2.1.4 SOURCE Type http://tools.ietf.org/html/rfc2426#section-2.1.4
+    if parentURI:
+        uri = joinURL(parentURI, record.fields[FieldName.uid].encode("utf-8") + ".vcf")
+
+        # seems like this should be in some standard place.
+        if config.EnableSSL and config.SSLPort:
+            if config.SSLPort == 443:
+                source = "https://{server}{uri}".format(server=config.ServerHostName, uri=uri)
+            else:
+                source = "https://{server}:{port}{uri}".format(server=config.ServerHostName, port=config.SSLPort, uri=uri)
+        else:
+            if config.HTTPPort == 80:
+                source = "https://{server}{uri}".format(server=config.ServerHostName, uri=uri)
+            else:
+                source = "https://{server}:{port}{uri}".format(server=config.ServerHostName, port=config.HTTPPort, uri=uri)
+        vcard.addProperty(Property("SOURCE", source))
+
+    #===================================================================
+    # 3.1 IDENTIFICATION TYPES http://tools.ietf.org/html/rfc2426#section-3.1
+    #===================================================================
+    # 3.1.1 FN
+    vcard.addProperty(Property("FN", record.fields[FieldName.fullNames][0].encode("utf-8")))
+
+    # 3.1.2 N
+    # TODO: Better parsing
+    fullNameParts = record.fields[FieldName.fullNames][0].split()
+    first = fullNameParts[0] if len(fullNameParts) >= 2 else None
+    last = fullNameParts[len(fullNameParts) - 1]
+    middle = fullNameParts[1] if len(fullNameParts) == 3 else None
+    prefix = None
+    suffix = None
+
+    nameObject = N(
+        first=first.encode("utf-8") if first else None,
+        last=last.encode("utf-8") if last else None,
+        middle=middle.encode("utf-8") if middle else None,
+        prefix=prefix.encode("utf-8") if prefix else None,
+        suffix=suffix.encode("utf-8") if suffix else None,
+    )
+    vcard.addProperty(Property("N", nameObject))
+
+    # 3.1.3 NICKNAME
+    nickname = record.fields.get(CalFieldName.abbreviatedName)
+    if nickname:
+        vcard.addProperty(Property("NICKNAME", nickname.encode("utf-8")))
+
+    # UNIMPLEMENTED
+    #     3.1.4 PHOTO
+    #     3.1.5 BDAY
+
+    #===========================================================================
+    # 3.2 Delivery Addressing Types http://tools.ietf.org/html/rfc2426#section-3.2
+    #===========================================================================
+    # 3.2.1 ADR
+
+    extended = record.fields.get(CalFieldName.floor)
+
+    #TODO: parse !
+    street = record.fields.get(CalFieldName.streetAddress)
+    city = None
+    region = None
+    postalcode = None
+    country = None
+
+    if extended or street or city or region or postalcode or country:
+        vcard.addProperty(
+            Property(
+                "ADR", Adr(
+                    #pobox = box,
+                    extended=extended.encode("utf-8") if extended else None,
+                    street=street.encode("utf-8") if street else None,
+                    locality=city.encode("utf-8") if city else None,
+                    region=region.encode("utf-8") if region else None,
+                    postalcode=postalcode.encode("utf-8") if postalcode else None,
+                    country=country.encode("utf-8") if country else None,
+                ),
+                params={"TYPE": ("WORK", "PREF", "POSTAL", "PARCEL",), }
+            )
+        )
+
+    # UNIMPLEMENTED
+    #     3.2.2 LABEL
+
+    #===================================================================
+    # 3.3 TELECOMMUNICATIONS ADDRESSING TYPES http://tools.ietf.org/html/rfc2426#section-3.3
+    #===================================================================
+    #
+    # UNIMPLEMENTED
+    #     3.3.1 TEL
+
+    # 3.3.2 EMAIL
+    preferredWorkParams = {"TYPE": ("WORK", "PREF", "INTERNET",), }
+    workParams = {"TYPE": ("WORK", "INTERNET",), }
+    params = preferredWorkParams
+    for emailAddress in record.fields.get(FieldName.emailAddresses, []):
+        addUniqueProperty(Property("EMAIL", emailAddress.encode("utf-8"), params=params), ignoredParameters={"TYPE": ("PREF",)})
+        params = workParams
+
+    # UNIMPLEMENTED:
+    #     3.3.3 MAILER
+    #
+    #===================================================================
+    # 3.4 GEOGRAPHICAL TYPES http://tools.ietf.org/html/rfc2426#section-3.4
+    #===================================================================
+    #
+    # UNIMPLEMENTED:
+    #     3.4.1 TZ
+    #
+    # 3.4.2 GEO
+    geographicLocation = record.fields.get(CalFieldName.geographicLocation)
+    if geographicLocation:
+        vcard.addProperty(Property("GEO", geographicLocation.encode("utf-8")))
+
+    #===================================================================
+    # 3.5 ORGANIZATIONAL TYPES http://tools.ietf.org/html/rfc2426#section-3.5
+    #===================================================================
+    #
+    # UNIMPLEMENTED:
+    #     3.5.1 TITLE
+    #     3.5.2 ROLE
+    #     3.5.3 LOGO
+    #     3.5.4 AGENT
+    #     3.5.5 ORG
+    #
+    #===================================================================
+    # 3.6 EXPLANATORY TYPES http://tools.ietf.org/html/rfc2426#section-3.6
+    #===================================================================
+    #
+    # UNIMPLEMENTED:
+    #     3.6.1 CATEGORIES
+    #     3.6.2 NOTE
+    #
+    # ADDED WITH CONTSTANT PROPERTIES:
+    #     3.6.3 PRODID
+    #
+    # UNIMPLEMENTED:
+    #     3.6.5 SORT-STRING
+    #     3.6.6 SOUND
+
+    # 3.6.7 UID
+    vcard.addProperty(Property("UID", record.fields[FieldName.uid].encode("utf-8")))
+
+    # UNIMPLEMENTED:
+    #     3.6.8 URL
+
+    # ADDED WITH CONTSTANT PROPERTIES:
+    #     3.6.9 VERSION
+
+    #===================================================================
+    # 3.7 SECURITY TYPES http://tools.ietf.org/html/rfc2426#section-3.7
+    #===================================================================
+    # UNIMPLEMENTED:
+    #     3.7.1 CLASS
+    #     3.7.2 KEY
+
+    #===================================================================
+    # X Properties
+    #===================================================================
+    # UNIMPLEMENTED:
+    #    X-<instant messaging type> such as:
+    #        "AIM", "FACEBOOK", "GAGU-GAGU", "GOOGLE TALK", "ICQ", "JABBER", "MSN", "QQ", "SKYPE", "YAHOO",
+    #    X-MAIDENNAME
+    #    X-PHONETIC-FIRST-NAME
+    #    X-PHONETIC-MIDDLE-NAME
+    #    X-PHONETIC-LAST-NAME
+    #    X-ABRELATEDNAMES
+
+    # X-ADDRESSBOOKSERVER-KIND
+    if kind == "group":
+        vcard.addProperty(Property("X-ADDRESSBOOKSERVER-KIND", kind))
+
+    # add members
+    # FIXME:  members() is a deferred, so all of vCardFromRecord is deferred.
+    for memberRecord in (yield record.members()):
+        vcard.addProperty(Property("X-ADDRESSBOOKSERVER-MEMBER", "urn:uuid:" + memberRecord.fields[FieldName.uid].encode("utf-8")))
+
+    #===================================================================
+    # vCard 4.0  http://tools.ietf.org/html/rfc6350
+    #===================================================================
+    # UNIMPLEMENTED:
+    #     6.4.3 IMPP http://tools.ietf.org/html/rfc6350#section-6.4.3
+    #
+    # 6.1.4 KIND http://tools.ietf.org/html/rfc6350#section-6.1.4
+    #
+    # see also: http://www.iana.org/assignments/vcard-elements/vcard-elements.xml
+    #
+    vcard.addProperty(Property("KIND", kind))
+
+    # one more X- related to kind
+    if kind == "org":
+        vcard.addProperty(Property("X-ABShowAs", "COMPANY"))
+
+    log.debug("vCardFromRecord: vcard=\n{vcard}", vcard=vcard)
+    returnValue(vcard)

Copied: CalendarServer/branches/users/sagen/move2who-5/txdav/who/wiki.py (from rev 13157, CalendarServer/branches/users/sagen/move2who-4/txdav/who/wiki.py)
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/wiki.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/wiki.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -0,0 +1,426 @@
+##
+# Copyright (c) 2006-2014 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.
+##
+
+"""
+Mac OS X Server Wiki directory service.
+"""
+
+__all__ = [
+    "WikiAccessLevel",
+    "DirectoryService",
+]
+
+import json
+from twext.internet.adaptendpoint import connect
+from twext.internet.gaiendpoint import GAIEndpoint
+from twext.internet.gaiendpoint import MultiFailure
+from twext.python.log import Logger
+from twext.who.directory import (
+    DirectoryService as BaseDirectoryService,
+    DirectoryRecord as BaseDirectoryRecord
+)
+from twext.who.idirectory import FieldName as BaseFieldName
+from twext.who.util import ConstantsContainer
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.python.constants import Names, NamedConstant
+from twisted.web.client import HTTPPageGetter, HTTPClientFactory
+from twisted.web.error import Error as WebError
+from txdav.who.directory import CalendarDirectoryRecordMixin
+from txdav.who.idirectory import FieldName
+from txdav.xml import element as davxml
+from txweb2 import responsecode
+from txweb2.auth.wrapper import UnauthorizedResponse
+from txweb2.dav.resource import TwistedACLInheritable
+from txweb2.http import HTTPError, StatusResponse
+
+
+log = Logger()
+
+
+# FIXME: Should this be Flags?
+class WikiAccessLevel(Names):
+    none  = NamedConstant()
+    read  = NamedConstant()
+    write = NamedConstant()
+
+
+
+class RecordType(Names):
+    macOSXServerWiki = NamedConstant()
+    macOSXServerWiki.description = u"Mac OS X Server Wiki"
+
+
+
+class DirectoryService(BaseDirectoryService):
+    """
+    Mac OS X Server Wiki directory service.
+    """
+
+    uidPrefix = u"[wiki]"
+
+    recordType = RecordType
+
+    fieldName = ConstantsContainer((
+        BaseFieldName,
+        FieldName,
+    ))
+
+
+    def __init__(self, realmName, wikiHost, wikiPort):
+        BaseDirectoryService.__init__(self, realmName)
+        self.wikiHost = wikiHost
+        self.wikiPort = wikiPort
+        self._recordsByName = {}
+
+
+    # This directory service is rather limited in its skills.
+    # We don't attempt to implement any expression handling (ie.
+    # recordsFromNonCompoundExpression), and only support a couple of the
+    # recordWith* convenience methods.
+
+    def _recordWithName(self, name):
+        record = self._recordsByName.get(name)
+
+        if record is not None:
+            return succeed(record)
+
+        # FIXME: RPC to the wiki and check for existance of a wiki with the
+        # given name...
+        #
+        # NOTE: Don't use the config module here; pass whatever info we need to
+        # __init__().
+        wikiExists = True
+
+        if wikiExists:
+            record = DirectoryRecord(
+                self,
+                {
+                    self.fieldName.uid: u"{}{}".format(self.uidPrefix, name),
+                    self.fieldName.recordType: RecordType.macOSXServerWiki,
+                    self.fieldName.shortNames: [name],
+                    self.fieldName.fullNames: [u"Wiki: {}".format(name)],
+                }
+            )
+            self._recordsByName[name] = record
+            return succeed(record)
+
+        return succeed(None)
+
+
+    def recordWithUID(self, uid):
+        if uid.startswith(self.uidPrefix):
+            return self._recordWithName(uid[len(self.uidPrefix):])
+        return succeed(None)
+
+
+    def recordWithShortName(self, recordType, shortName):
+        if recordType is RecordType.macOSXServerWiki:
+            return self._recordWithName(shortName)
+        return succeed(None)
+
+
+    def recordsFromExpression(self, expression, records=None):
+        return succeed(())
+
+
+
+class DirectoryRecord(BaseDirectoryRecord, CalendarDirectoryRecordMixin):
+    """
+    Mac OS X Server Wiki directory record.
+    """
+
+    log = Logger()
+
+
+    @property
+    def name(self):
+        return self.shortNames[0]
+
+
+    @inlineCallbacks
+    def accessForRecord(self, record):
+        """
+        Look up the access level for a record in this wiki.
+
+        @param user: The record to check access for.  A value of None means
+            unauthenticated
+        """
+        if record is None:
+            uid = u"unauthenticated"
+        else:
+            uid = record.uid
+
+        try:
+            # FIXME: accessForUserToWiki() API is lame.
+            # There are no other callers except the old directory API, so
+            # nuke it from the originating module and move that logic here
+            # once the old API is removed.
+            # When we do that note: isn't there a getPage() in twisted.web?
+
+            access = yield accessForUserToWiki(
+                uid, self.shortNames[0],
+                host=self.service.wikiHost,
+                port=self.service.wikiPort,
+            )
+
+        except MultiFailure as e:
+            self.log.error(
+                "Unable to look up access for record {record} "
+                "in wiki {log_source}: {error}",
+                record=record, error=e
+            )
+
+        except WebError as e:
+            status = int(e.status)
+
+            if status == responsecode.FORBIDDEN:  # Unknown user
+                self.log.debug(
+                    "No such record (according to wiki): {record}",
+                    record=record, error=e
+                )
+                returnValue(WikiAccessLevel.none)
+
+            if status == responsecode.NOT_FOUND:  # Unknown wiki
+                self.log.error(
+                    "No such wiki: {log_source.name}",
+                    record=record, error=e
+                )
+                returnValue(WikiAccessLevel.none)
+
+            self.log.error(
+                "Unable to look up wiki access: {error}",
+                record=record, error=e
+            )
+
+        try:
+            returnValue({
+                "no-access": WikiAccessLevel.none,
+                "read": WikiAccessLevel.read,
+                "write": WikiAccessLevel.write,
+                "admin": WikiAccessLevel.write,
+            }[access])
+
+        except KeyError:
+            self.log.error("Unknown wiki access level: {level}", level=access)
+            returnValue(WikiAccessLevel.none)
+
+
+ at inlineCallbacks
+def getWikiACL(resource, request):
+    """
+    Ask the wiki server we're paired with what level of access the authnUser
+    has.
+
+    Returns an ACL.
+
+    Wiki authentication is a bit tricky because the end-user accessing a group
+    calendar may not actually be enabled for calendaring.  Therefore in that
+    situation, the authzUser will have been replaced with the wiki principal
+    in locateChild( ), so that any changes the user makes will have the wiki
+    as the originator.  The authnUser will always be the end-user.
+    """
+    from twistedcaldav.directory.principal import DirectoryPrincipalResource
+
+    if (
+        not hasattr(resource, "record") or
+        resource.record.recordType != RecordType.macOSXServerWiki
+    ):
+        returnValue(None)
+
+    if hasattr(request, 'wikiACL'):
+        returnValue(request.wikiACL)
+
+    wikiRecord = resource.record
+    wikiID = wikiRecord.shortNames[0]
+    userRecord = None
+
+    try:
+        url = str(request.authnUser.children[0])
+        principal = (yield request.locateResource(url))
+        if isinstance(principal, DirectoryPrincipalResource):
+            userRecord = principal.record
+    except:
+        # TODO: better error handling
+        pass
+
+    try:
+        access = yield wikiRecord.accessForRecord(userRecord)
+
+        # The ACL we returns has ACEs for the end-user and the wiki principal
+        # in case authzUser is the wiki principal.
+        if access == WikiAccessLevel.read:
+            request.wikiACL = davxml.ACL(
+                davxml.ACE(
+                    request.authnUser,
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+
+                        # We allow write-properties so that direct sharees can
+                        # change e.g. calendar color properties
+                        davxml.Privilege(davxml.WriteProperties()),
+                    ),
+                    TwistedACLInheritable(),
+                ),
+                davxml.ACE(
+                    davxml.Principal(
+                        davxml.HRef.fromString(
+                            "/principals/wikis/{}/".format(wikiID)
+                        )
+                    ),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+                    ),
+                    TwistedACLInheritable(),
+                )
+            )
+            returnValue(request.wikiACL)
+
+        elif access == WikiAccessLevel.write:
+            request.wikiACL = davxml.ACL(
+                davxml.ACE(
+                    request.authnUser,
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+                        davxml.Privilege(davxml.Write()),
+                    ),
+                    TwistedACLInheritable(),
+                ),
+                davxml.ACE(
+                    davxml.Principal(
+                        davxml.HRef.fromString(
+                            "/principals/wikis/{}/".format(wikiID)
+                        )
+                    ),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+                        davxml.Privilege(davxml.Write()),
+                    ),
+                    TwistedACLInheritable(),
+                )
+            )
+            returnValue(request.wikiACL)
+
+        else:  # "no-access":
+
+            if userRecord is None:
+                # Return a 401 so they have an opportunity to log in
+                response = (yield UnauthorizedResponse.makeResponse(
+                    request.credentialFactories,
+                    request.remoteAddr,
+                ))
+                raise HTTPError(response)
+
+            raise HTTPError(
+                StatusResponse(
+                    responsecode.FORBIDDEN,
+                    "You are not allowed to access this wiki"
+                )
+            )
+
+    except HTTPError:
+        # pass through the HTTPError we might have raised above
+        raise
+
+    except Exception as e:
+        log.error("Wiki ACL lookup failed: {error}", error=e)
+        raise HTTPError(StatusResponse(
+            responsecode.SERVICE_UNAVAILABLE, "Wiki ACL lookup failed"
+        ))
+
+
+
+class WebAuthError(RuntimeError):
+    """
+    Error in web auth
+    """
+
+
+
+ at inlineCallbacks
+def uidForAuthToken(token, host="localhost", port=80):
+    """
+    Send a GET request to the web auth service to retrieve the user record
+    uid associated with the provided auth token.
+
+    @param token: An auth token, usually passed in via cookie when webcal
+        makes a request.
+    @type token: C{str}
+    @return: deferred returning a uid (C{str}) if successful, or
+        will raise WebAuthError otherwise.
+    """
+    url = "http://%s:%d/auth/verify?auth_token=%s" % (host, port, token,)
+    jsonResponse = (yield _getPage(url, host, port))
+    try:
+        response = json.loads(jsonResponse)
+    except Exception, e:
+        log.error(
+            "Error parsing JSON response from webauth: {resp} {error}",
+            resp=jsonResponse, error=str(e)
+        )
+        raise WebAuthError("Could not look up token: %s" % (token,))
+    if response["succeeded"]:
+        returnValue(response["generated_uid"])
+    else:
+        raise WebAuthError("Could not look up token: %s" % (token,))
+
+
+
+def accessForUserToWiki(user, wiki, host="localhost", port=4444):
+    """
+    Send a GET request to the wiki collabd service to retrieve the access level
+    the given user (uid) has to the given wiki (in wiki short-name
+    form).
+
+    @param user: The UID of the user
+    @type user: C{str}
+    @param wiki: The short name of the wiki
+    @type wiki: C{str}
+    @return: deferred returning a access level (C{str}) if successful, or
+        if the user is not recognized a twisted.web.error.Error with
+        status FORBIDDEN will errBack; an unknown wiki will have a status
+        of NOT_FOUND
+    """
+    url = "http://%s:%s/cal/accessLevelForUserWikiCalendar/%s/%s" % (
+        host, port, user, wiki
+    )
+    return _getPage(url, host, port)
+
+
+
+# FIXME: Why don't we use twisted.web.
+def _getPage(url, host, port):
+    """
+    Fetch the body of the given url via HTTP, connecting to the given host
+    and port.
+
+    @param url: The URL to GET
+    @type url: C{str}
+    @param host: The hostname to connect to
+    @type host: C{str}
+    @param port: The port number to connect to
+    @type port: C{int}
+    @return: A deferred; upon 200 success the body of the response is returned,
+        otherwise a twisted.web.error.Error is the result.
+    """
+    factory = HTTPClientFactory(url)
+    factory.protocol = HTTPPageGetter
+    connect(GAIEndpoint(reactor, host, port), factory)
+    return factory.deferred

Modified: CalendarServer/branches/users/sagen/move2who-5/txdav/who/xml.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txdav/who/xml.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txdav/who/xml.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -105,8 +105,30 @@
         AutoScheduleMode.acceptIfFreeDeclineIfBusy
     )
 
+    # For "locations", i.e., scheduled spaces:
 
+    capacity = ValueConstant(u"capacity")
+    capacity.fieldName = FieldName.capacity
 
+    floor = ValueConstant(u"floor")
+    floor.fieldName = FieldName.floor
+
+    associatedAddress = ValueConstant(u"associated-address")
+    associatedAddress.fieldName = FieldName.associatedAddress
+
+    # For "addresses", i.e., non-scheduled areas containing locations:
+
+    abbreviatedName = ValueConstant(u"abbreviated-name")
+    abbreviatedName.fieldName = FieldName.abbreviatedName
+
+    streetAddress = ValueConstant(u"street-address")
+    streetAddress.fieldName = FieldName.streetAddress
+
+    geographicLocation = ValueConstant(u"geographic-location")
+    geographicLocation.fieldName = FieldName.geographicLocation
+
+
+
 class Attribute(Values):
     """
     XML calendar and contacts attribute names.
@@ -143,10 +165,14 @@
         (BaseDirectoryService.recordType, RecordType)
     )
 
-    fieldName = ConstantsContainer(
-        (BaseDirectoryService.fieldName, FieldName)
-    )
+    # MOVE2WHO: Wilfredo had added augment fields into xml, which does make
+    # some sense, but for backwards compatibility right now I will take those
+    # out, and rely on a separate augment service
 
+    # fieldName = ConstantsContainer(
+    #     (BaseDirectoryService.fieldName, FieldName)
+    # )
+
     # XML schema constants
 
     element = ConstantsContainer(

Modified: CalendarServer/branches/users/sagen/move2who-5/txweb2/channel/http.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txweb2/channel/http.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txweb2/channel/http.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -137,10 +137,10 @@
     subclass, it can parse either the client side or the server side of the
     connection.
     """
-    
+
     # Class config:
     parseCloseAsEnd = False
-    
+
     # Instance vars
     chunkedIn = False
     headerlen = 0
@@ -173,12 +173,12 @@
     #  channel.pauseProducing()
     #  channel.resumeProducing()
     #  channel.stopProducing()
-    
-    
+
+
     def __init__(self, channel):
         self.inHeaders = http_headers.Headers()
         self.channel = channel
-        
+
     def lineReceived(self, line):
         if self.chunkedIn:
             # Parsing a chunked input
@@ -208,7 +208,7 @@
                 self.chunkedIn = 1
             elif self.chunkedIn == 3:
                 # TODO: support Trailers (maybe! but maybe not!)
-                
+
                 # After getting the final "0" chunk we're here, and we *EAT MERCILESSLY*
                 # any trailer headers sent, and wait for the blank line to terminate the
                 # request.
@@ -237,7 +237,7 @@
             self.headerlen += len(line)
             if self.headerlen > self.channel.maxHeaderLength:
                 self._abortWithError(responsecode.BAD_REQUEST, 'Headers too long.')
-            
+
             if line[0] in ' \t':
                 # Append a header continuation
                 self.partialHeader += line
@@ -262,7 +262,7 @@
                 # NOTE: in chunked mode, self.length is the size of the current chunk,
                 # so we still have more to read.
                 self.chunkedIn = 2 # Read next chunksize
-            
+
             channel.setLineMode(extraneous)
 
 
@@ -293,13 +293,13 @@
         # Set connection parameters from headers
         self.setConnectionParams(connHeaders)
         self.connHeaders = connHeaders
-        
+
     def allContentReceived(self):
         self.finishedReading = True
         self.channel.requestReadFinished(self)
         self.handleContentComplete()
-        
-        
+
+
     def splitConnectionHeaders(self):
         """
         Split off connection control headers from normal headers.
@@ -382,7 +382,7 @@
         # Okay, now implement section 4.4 Message Length to determine
         # how to find the end of the incoming HTTP message.
         transferEncoding = connHeaders.getHeader('transfer-encoding')
-        
+
         if transferEncoding:
             if transferEncoding[-1] == 'chunked':
                 # Chunked
@@ -394,7 +394,7 @@
                 # client->server data. (Well..it could actually, since TCP has half-close
                 # but the HTTP spec says it can't, so we'll pretend it's right.)
                 self._abortWithError(responsecode.BAD_REQUEST, "Transfer-Encoding received without chunked in last position.")
-            
+
             # TODO: support gzip/etc encodings.
             # FOR NOW: report an error if the client uses any encodings.
             # They shouldn't, because we didn't send a TE: header saying it's okay.
@@ -423,23 +423,23 @@
 
         # Set the calculated persistence
         self.channel.setReadPersistent(readPersistent)
-        
+
     def abortParse(self):
         # If we're erroring out while still reading the request
         if not self.finishedReading:
             self.finishedReading = True
             self.channel.setReadPersistent(False)
             self.channel.requestReadFinished(self)
-        
+
     # producer interface
     def pauseProducing(self):
         if not self.finishedReading:
             self.channel.pauseProducing()
-        
+
     def resumeProducing(self):
         if not self.finishedReading:
             self.channel.resumeProducing()
-       
+
     def stopProducing(self):
         if not self.finishedReading:
             self.channel.stopProducing()
@@ -449,13 +449,13 @@
     It is responsible for all the low-level connection oriented behavior.
     Thus, it takes care of keep-alive, de-chunking, etc., and passes
     the non-connection headers on to the user-level Request object."""
-    
+
     command = path = version = None
     queued = 0
     request = None
-    
+
     out_version = "HTTP/1.1"
-    
+
     def __init__(self, channel, queued=0):
         HTTPParser.__init__(self, channel)
         self.queued=queued
@@ -466,14 +466,14 @@
             self.transport = StringTransport()
         else:
             self.transport = self.channel.transport
-        
+
         # set the version to a fallback for error generation
         self.version = (1,0)
 
 
     def gotInitialLine(self, initialLine):
         parts = initialLine.split()
-        
+
         # Parse the initial request line
         if len(parts) != 3:
             if len(parts) == 1:
@@ -490,9 +490,9 @@
                 raise ValueError()
         except ValueError:
             self._abortWithError(responsecode.BAD_REQUEST, "Unknown protocol: %s" % strversion)
-        
+
         self.version = protovers[1:3]
-        
+
         # Ensure HTTP 0 or HTTP 1.
         if self.version[0] > 1:
             self._abortWithError(responsecode.HTTP_VERSION_NOT_SUPPORTED, 'Only HTTP 0.9 and HTTP 1.x are supported.')
@@ -511,18 +511,18 @@
 
     def processRequest(self):
         self.request.process()
-        
+
     def handleContentChunk(self, data):
         self.request.handleContentChunk(data)
-        
+
     def handleContentComplete(self):
         self.request.handleContentComplete()
-        
+
 ############## HTTPChannelRequest *RESPONSE* methods #############
     producer = None
     chunkedOut = False
     finished = False
-    
+
     ##### Request Callbacks #####
     def writeIntermediateResponse(self, code, headers=None):
         if self.version >= (1,1):
@@ -530,15 +530,15 @@
 
     def writeHeaders(self, code, headers):
         self._writeHeaders(code, headers, True)
-        
+
     def _writeHeaders(self, code, headers, addConnectionHeaders):
         # HTTP 0.9 doesn't have headers.
         if self.version[0] == 0:
             return
-        
+
         l = []
         code_message = responsecode.RESPONSES.get(code, "Unknown Status")
-        
+
         l.append('%s %s %s\r\n' % (self.out_version, code,
                                    code_message))
         if headers is not None:
@@ -557,16 +557,16 @@
                 else:
                     # Cannot use persistent connections if we can't do chunking
                     self.channel.dropQueuedRequests()
-            
+
             if self.channel.isLastRequest(self):
                 l.append("%s: %s\r\n" % ('Connection', 'close'))
             elif self.version < (1,1):
                 l.append("%s: %s\r\n" % ('Connection', 'Keep-Alive'))
-        
+
         l.append("\r\n")
         self.transport.writeSequence(l)
-        
-    
+
+
     def write(self, data):
         if not data:
             return
@@ -574,17 +574,17 @@
             self.transport.writeSequence(("%X\r\n" % len(data), data, "\r\n"))
         else:
             self.transport.write(data)
-        
+
     def finish(self):
         """We are finished writing data."""
         if self.finished:
             warnings.warn("Warning! request.finish called twice.", stacklevel=2)
             return
-        
+
         if self.chunkedOut:
             # write last chunk and closing CRLF
             self.transport.write("0\r\n\r\n")
-        
+
         self.finished = True
         if not self.queued:
             self._cleanup()
@@ -596,7 +596,7 @@
         the writing side alone. This is mostly for internal use by
         the HTTP request parsing logic, so that it can call an error
         page generator.
-        
+
         Otherwise, completely shut down the connection.
         """
         self.abortParse()
@@ -604,7 +604,7 @@
             if self.producer:
                 self.producer.stopProducing()
                 self.unregisterProducer()
-            
+
             self.finished = True
             if self.queued:
                 self.transport.reset()
@@ -617,14 +617,14 @@
 
     def getRemoteHost(self):
         return self.channel.transport.getPeer()
-    
+
     ##### End Request Callbacks #####
 
     def _abortWithError(self, errorcode, text=''):
         """Handle low level protocol errors."""
         headers = http_headers.Headers()
         headers.setHeader('content-length', len(text)+1)
-        
+
         self.abortConnection(closeWrite=False)
         self.writeHeaders(errorcode, headers)
         self.write(text)
@@ -632,7 +632,7 @@
         self.finish()
         log.warn("Aborted request (%d) %s" % (errorcode, text))
         raise AbortedException
-    
+
     def _cleanup(self):
         """Called when have finished responding and are no longer queued."""
         if self.producer:
@@ -640,7 +640,7 @@
             self.unregisterProducer()
         self.channel.requestWriteFinished(self)
         del self.transport
-        
+
     # methods for channel - end users should not use these
 
     def noLongerQueued(self):
@@ -674,12 +674,12 @@
     def registerProducer(self, producer, streaming):
         """Register a producer.
         """
-        
+
         if self.producer:
             raise ValueError, "registering producer %s before previous one (%s) was unregistered" % (producer, self.producer)
-        
+
         self.producer = producer
-        
+
         if self.queued:
             producer.pauseProducing()
         else:
@@ -698,7 +698,7 @@
             self.producer = None
         if self.request:
             self.request.connectionLost(reason)
-    
+
 class HTTPChannel(basic.LineReceiver, policies.TimeoutMixin, object):
     """A receiver for HTTP requests. Handles splitting up the connection
     for the multiple HTTPChannelRequests that may be in progress on this
@@ -714,11 +714,11 @@
     the client.
 
     """
-    
+
     implements(interfaces.IHalfCloseableProtocol)
-    
+
     ## Configuration parameters. Set in instances or subclasses.
-    
+
     # How many simultaneous requests to handle.
     maxPipeline = 4
 
@@ -736,35 +736,35 @@
 
     # Allow persistent connections?
     allowPersistentConnections = True
-    
+
     # ChannelRequest
     chanRequestFactory = HTTPChannelRequest
     requestFactory = http.Request
-    
-    
+
+
     _first_line = 2
     readPersistent = PERSIST_PIPELINE
-    
+
     _readLost = False
     _writeLost = False
-    
+
     _abortTimer = None
     chanRequest = None
 
     def _callLater(self, secs, fun):
         reactor.callLater(secs, fun)
-    
+
     def __init__(self):
         # the request queue
         self.requests = []
-        
+
     def connectionMade(self):
         self._secure = interfaces.ISSLTransport(self.transport, None) is not None
         address = self.transport.getHost()
         self._host = _cachedGetHostByAddr(address.host)
         self.setTimeout(self.inputTimeOut)
         self.factory.addConnectedChannel(self)
-    
+
     def lineReceived(self, line):
         if self._first_line:
             self.setTimeout(self.inputTimeOut)
@@ -779,13 +779,13 @@
             if not line and self._first_line == 1:
                 self._first_line = 2
                 return
-            
+
             self._first_line = 0
-            
+
             if not self.allowPersistentConnections:
                 # Don't allow a second request
                 self.readPersistent = False
-                
+
             try:
                 self.chanRequest = self.chanRequestFactory(self, len(self.requests))
                 self.requests.append(self.chanRequest)
@@ -801,7 +801,7 @@
     def lineLengthExceeded(self, line):
         if self._first_line:
             # Fabricate a request object to respond to the line length violation.
-            self.chanRequest = self.chanRequestFactory(self, 
+            self.chanRequest = self.chanRequestFactory(self,
                                                        len(self.requests))
             self.requests.append(self.chanRequest)
             self.chanRequest.gotInitialLine("GET fake HTTP/1.0")
@@ -809,7 +809,7 @@
             self.chanRequest.lineLengthExceeded(line, self._first_line)
         except AbortedException:
             pass
-            
+
     def rawDataReceived(self, data):
         self.setTimeout(self.inputTimeOut)
         try:
@@ -821,17 +821,17 @@
         if(self.readPersistent is PERSIST_NO_PIPELINE or
            len(self.requests) >= self.maxPipeline):
             self.pauseProducing()
-        
+
         # reset state variables
         self._first_line = 1
         self.chanRequest = None
         self.setLineMode()
-        
+
         # Set an idle timeout, in case this request takes a long
         # time to finish generating output.
         if len(self.requests) > 0:
             self.setTimeout(self.idleTimeOut)
-        
+
     def _startNextRequest(self):
         # notify next request, if present, it can start writing
         del self.requests[0]
@@ -840,7 +840,7 @@
             self.transport.loseConnection()
         elif self.requests:
             self.requests[0].noLongerQueued()
-            
+
             # resume reading if allowed to
             if(not self._readLost and
                self.readPersistent is not PERSIST_NO_PIPELINE and
@@ -866,11 +866,11 @@
         for request in self.requests[1:]:
             request.connectionLost(None)
         del self.requests[1:]
-    
+
     def isLastRequest(self, request):
         # Is this channel handling the last possible request
         return not self.readPersistent and self.requests[-1] == request
-    
+
     def requestWriteFinished(self, request):
         """Called by first request in queue when it is done."""
         if request != self.requests[0]: raise TypeError
@@ -878,7 +878,7 @@
         # Don't del because we haven't finished cleanup, so,
         # don't want queue len to be 0 yet.
         self.requests[0] = None
-        
+
         if self.readPersistent or len(self.requests) > 1:
             # Do this in the next reactor loop so as to
             # not cause huge call stacks with fast
@@ -910,26 +910,26 @@
             self._abortTimer = None
             self.transport.loseConnection()
             return
-        
+
         # If between requests, drop connection
         # when all current requests have written their data.
         self._readLost = True
         if not self.requests:
             # No requests in progress, lose now.
             self.transport.loseConnection()
-            
+
         # If currently in the process of reading a request, this is
         # probably a client abort, so lose the connection.
         if self.chanRequest:
             self.transport.loseConnection()
-        
+
     def connectionLost(self, reason):
         self.factory.removeConnectedChannel(self)
 
         self._writeLost = True
         self.readConnectionLost()
         self.setTimeout(None)
-        
+
         # Tell all requests to abort.
         for request in self.requests:
             if request is not None:
@@ -963,7 +963,7 @@
     """
 
     protocol = HTTPChannel
-    
+
     protocolArgs = None
 
     def __init__(self, requestFactory, maxRequests=600, **kwargs):
@@ -977,9 +977,9 @@
     def buildProtocol(self, addr):
         if self.outstandingRequests >= self.maxRequests:
             return OverloadedServerProtocol()
-        
+
         p = protocol.ServerFactory.buildProtocol(self, addr)
-        
+
         for arg,value in self.protocolArgs.iteritems():
             setattr(p, arg, value)
         return p
@@ -1050,19 +1050,19 @@
         return p
 
 class HTTPLoggingChannelRequest(HTTPChannelRequest):
-    
+
     class TransportLoggingWrapper(object):
-        
+
         def __init__(self, transport, logData):
-            
+
             self.transport = transport
             self.logData = logData
-            
+
         def write(self, data):
             if self.logData is not None and data:
                 self.logData.append(data)
             self.transport.write(data)
-            
+
         def writeSequence(self, seq):
             if self.logData is not None and seq:
                 self.logData.append(''.join(seq))
@@ -1075,7 +1075,7 @@
         def __init__(self):
             self.request = []
             self.response = []
-            
+
     def __init__(self, channel, queued=0):
         super(HTTPLoggingChannelRequest, self).__init__(channel, queued)
 
@@ -1093,7 +1093,7 @@
         super(HTTPLoggingChannelRequest, self).gotInitialLine(initialLine)
 
     def lineReceived(self, line):
-        
+
         if self.logData is not None:
             # We don't want to log basic credentials
             loggedLine = line
@@ -1105,13 +1105,13 @@
         super(HTTPLoggingChannelRequest, self).lineReceived(line)
 
     def handleContentChunk(self, data):
-        
+
         if self.logData is not None:
             self.logData.request.append(data)
         super(HTTPLoggingChannelRequest, self).handleContentChunk(data)
-        
+
     def handleContentComplete(self):
-        
+
         if self.logData is not None:
             doneTime = time.time()
             self.logData.request.append("\r\n\r\n>>>> Request complete at: %.3f (elapsed: %.1f ms)" % (doneTime, 1000 * (doneTime - self.startTime),))
@@ -1124,7 +1124,7 @@
         super(HTTPLoggingChannelRequest, self).writeHeaders(code, headers)
 
     def finish(self):
-        
+
         super(HTTPLoggingChannelRequest, self).finish()
 
         if self.logData is not None:

Modified: CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/method/report_expand.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/method/report_expand.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/method/report_expand.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -8,10 +8,10 @@
 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 # copies of the Software, and to permit persons to whom the Software is
 # furnished to do so, subject to the following conditions:
-# 
+#
 # The above copyright notice and this permission notice shall be included in all
 # copies or substantial portions of the Software.
-# 
+#
 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
@@ -48,7 +48,7 @@
 def report_DAV__expand_property(self, request, expand_property):
     """
     Generate an expand-property REPORT. (RFC 3253, section 3.8)
-    
+
     TODO: for simplicity we will only support one level of expansion.
     """
     # Verify root element
@@ -61,7 +61,7 @@
     if depth != "0":
         log.error("Non-zero depth is not allowed: %s" % (depth,))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Depth %s not allowed" % (depth,)))
-    
+
     #
     # Get top level properties to expand and make sure we only have one level
     #
@@ -70,7 +70,7 @@
     for property in expand_property.children:
         namespace = property.attributes.get("namespace", dav_namespace)
         name      = property.attributes.get("name", "")
-        
+
         # Make sure children have no children
         props_to_find = []
         for child in property.children:
@@ -93,14 +93,14 @@
         responsecode.OK        : [],
         responsecode.NOT_FOUND : [],
     }
-    
+
     filteredaces = None
     lastParent = None
 
     for qname in properties.iterkeys():
         try:
             prop = (yield self.readProperty(qname, request))
-            
+
             # Form the PROPFIND-style DAV:prop element we need later
             props_to_return = element.PropertyContainer(*properties[qname])
 
@@ -108,27 +108,27 @@
             responses = []
             for href in prop.children:
                 if isinstance(href, element.HRef):
-                    
+
                     # Locate the Href resource and its parent
                     resource_uri = str(href)
                     child = (yield request.locateResource(resource_uri))
-    
+
                     if not child or not child.exists():
                         responses.append(element.StatusResponse(href, element.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
                     parent = (yield request.locateResource(parentForURL(resource_uri)))
-    
+
                     # Check privileges on parent - must have at least DAV:read
                     try:
                         yield parent.checkPrivileges(request, (element.Read(),))
                     except AccessDeniedError:
                         responses.append(element.StatusResponse(href, element.Status.fromResponseCode(responsecode.FORBIDDEN)))
                         continue
-                    
+
                     # Cache the last parent's inherited aces for checkPrivileges optimization
                     if lastParent != parent:
                         lastParent = parent
-                
+
                         # Do some optimisation of access control calculation by determining any inherited ACLs outside of
                         # the child resource loop and supply those to the checkPrivileges on each child.
                         filteredaces = (yield parent.inheritedACEsforChildren(request))
@@ -139,7 +139,7 @@
                     except AccessDeniedError:
                         responses.append(element.StatusResponse(href, element.Status.fromResponseCode(responsecode.FORBIDDEN)))
                         continue
-            
+
                     # Now retrieve all the requested properties on the HRef resource
                     yield prop_common.responseForHref(
                         request,
@@ -149,13 +149,16 @@
                         prop_common.propertyListForResource,
                         props_to_return,
                     )
-            
+
             prop.children = responses
             properties_by_status[responsecode.OK].append(prop)
         except:
             f = Failure()
 
-            log.error("Error reading property %r for resource %s: %s" % (qname, request.uri, f.value))
+            log.error(
+                "Error reading property {qname} for resource {req}: {failure}",
+                qname=qname, req=request.uri, failure=f.value
+            )
 
             status = statusForFailure(f, "getting property: %s" % (qname,))
             if status not in properties_by_status: properties_by_status[status] = []

Modified: CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/resource.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/resource.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/resource.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -997,6 +997,7 @@
             if the authentication scheme is unsupported, or the
             credentials provided by the request are not valid.
         """
+
         # Bypass normal authentication if its already been done (by SACL check)
         if (
             hasattr(request, "authnUser") and
@@ -1134,7 +1135,7 @@
         # The default behaviour is no ACL; we should inherit from the parent
         # collection.
         #
-        return element.ACL()
+        return succeed(element.ACL())
 
 
     def setAccessControlList(self, acl):
@@ -1360,6 +1361,7 @@
         @return: a L{Deferred} that callbacks with C{None} or errbacks
             with an L{AccessDeniedError}
         """
+
         if principal is None:
             principal = self.currentPrincipal(request)
 
@@ -1509,7 +1511,7 @@
                 # If we get to the root without any ACLs, then use the default.
                 acl = self.defaultRootAccessControlList()
             else:
-                acl = self.defaultAccessControlList()
+                acl = yield self.defaultAccessControlList()
 
         # Dynamically update privileges for those ace's that are inherited.
         if inheritance:
@@ -1618,6 +1620,7 @@
         return []
 
 
+    @inlineCallbacks
     def principalsForAuthID(self, request, authid):
         """
         Return authentication and authorization principal identifiers
@@ -1637,16 +1640,16 @@
             HTTPError(responsecode.FORBIDDEN) if the principal isn't
             found.
         """
-        authnPrincipal = self.findPrincipalForAuthID(authid)
+        authnPrincipal = yield self.findPrincipalForAuthID(authid)
 
         if authnPrincipal is None:
-            return succeed((None, None))
+            returnValue((None, None))
 
-        d = self.authorizationPrincipal(request, authid, authnPrincipal)
-        d.addCallback(lambda authzPrincipal: (authnPrincipal, authzPrincipal))
-        return d
+        authzPrincipal = yield self.authorizationPrincipal(request, authid, authnPrincipal)
+        returnValue((authnPrincipal, authzPrincipal))
 
 
+    @inlineCallbacks
     def findPrincipalForAuthID(self, authid):
         """
         Return authentication and authorization principal identifiers
@@ -1662,10 +1665,10 @@
             found return None.
         """
         for collection in self.principalCollections():
-            principal = collection.principalForUser(authid)
+            principal = yield collection.principalForUser(authid)
             if principal is not None:
-                return principal
-        return None
+                returnValue(principal)
+        returnValue(None)
 
 
     def authorizationPrincipal(self, request, authid, authnPrincipal):

Modified: CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/util.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/util.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txweb2/dav/util.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -41,9 +41,8 @@
     "bindMethods",
 ]
 
-import urllib
 from urlparse import urlsplit, urlunsplit
-import posixpath # Careful; this module is not documented as public API
+import posixpath  # Careful; this module is not documented as public API
 
 from twisted.python.failure import Failure
 from twisted.internet.defer import succeed
@@ -125,13 +124,13 @@
                 count += 1
             path = path[count - 1:]
 
-        return path
+        return path.encode("utf-8")
 
     (scheme, host, path, query, fragment) = urlsplit(cleanup(url))
 
-    path = cleanup(posixpath.normpath(urllib.unquote(path)))
+    path = cleanup(posixpath.normpath(path))
 
-    return urlunsplit((scheme, host, urllib.quote(path), query, fragment))
+    return urlunsplit((scheme, host, path, query, fragment))
 
 
 

Modified: CalendarServer/branches/users/sagen/move2who-5/txweb2/server.py
===================================================================
--- CalendarServer/branches/users/sagen/move2who-5/txweb2/server.py	2014-04-04 17:05:48 UTC (rev 13157)
+++ CalendarServer/branches/users/sagen/move2who-5/txweb2/server.py	2014-04-04 17:20:27 UTC (rev 13158)
@@ -192,7 +192,7 @@
                        error.defaultErrorHandler, defaultHeadersFilter]
 
     def __init__(self, *args, **kw):
-        
+
         self.timeStamps = [("t", time.time(),)]
 
         if kw.has_key('site'):
@@ -308,10 +308,10 @@
         clients into using an inappropriate scheme for subsequent requests. What we should do is
         take the port number from the Host header or request-URI and map that to the scheme that
         matches the service we configured to listen on that port.
- 
+
         @param port: the port number to test
         @type port: C{int}
-        
+
         @return: C{True} if scheme is https (secure), C{False} otherwise
         @rtype: C{bool}
         """
@@ -322,7 +322,7 @@
                 return True
             elif port in self.site.BindSSLPorts:
                 return True
-        
+
         return False
 
     def _fixupURLParts(self):
@@ -558,7 +558,7 @@
                 break
             else:
                 postSegments.insert(0, preSegments.pop())
-        
+
         if cachedParent is None:
             cachedParent = self.site.resource
             postSegments = segments[1:]
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140404/0008fe7b/attachment-0001.html>


More information about the calendarserver-changes mailing list