<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[13988] CalendarServer/trunk</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/13988">13988</a></dd>
<dt>Author</dt> <dd>cdaboo@apple.com</dd>
<dt>Date</dt> <dd>2014-09-22 10:00:48 -0700 (Mon, 22 Sep 2014)</dd>
</dl>
<h3>Log Message</h3>
<pre>Enable addMemberts/removeMembers functionality in the directory and DPS. Support those methods with new commands in calendarserver_manage_principals. Add a new control API resource to also expose those operations via HTTP for CDT testing.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#CalendarServertrunkcalendarservertaputilpy">CalendarServer/trunk/calendarserver/tap/util.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolsprincipalspy">CalendarServer/trunk/calendarserver/tools/principals.py</a></li>
<li><a href="#CalendarServertrunkcalendarservertoolstesttest_principalspy">CalendarServer/trunk/calendarserver/tools/test/test_principals.py</a></li>
<li><a href="#CalendarServertrunkconfcaldavdtestplist">CalendarServer/trunk/conf/caldavd-test.plist</a></li>
<li><a href="#CalendarServertrunktwistedcaldavdirectoryaugmentpy">CalendarServer/trunk/twistedcaldav/directory/augment.py</a></li>
<li><a href="#CalendarServertrunktwistedcaldavstdconfigpy">CalendarServer/trunk/twistedcaldav/stdconfig.py</a></li>
<li><a href="#CalendarServertrunktxdavdpsclientpy">CalendarServer/trunk/txdav/dps/client.py</a></li>
<li><a href="#CalendarServertrunktxdavdpscommandspy">CalendarServer/trunk/txdav/dps/commands.py</a></li>
<li><a href="#CalendarServertrunktxdavdpsserverpy">CalendarServer/trunk/txdav/dps/server.py</a></li>
<li><a href="#CalendarServertrunktxdavdpstesttest_clientpy">CalendarServer/trunk/txdav/dps/test/test_client.py</a></li>
<li><a href="#CalendarServertrunktxdavwhoaugmentpy">CalendarServer/trunk/txdav/who/augment.py</a></li>
<li><a href="#CalendarServertrunktxdavwhoxmlpy">CalendarServer/trunk/txdav/who/xml.py</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#CalendarServertrunktwistedcaldavcontrolapipy">CalendarServer/trunk/twistedcaldav/controlapi.py</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="CalendarServertrunkcalendarservertaputilpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tap/util.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tap/util.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/calendarserver/tap/util.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -61,6 +61,7 @@
</span><span class="cx"> from twext.enterprise.ienterprise import POSTGRES_DIALECT
</span><span class="cx"> from twistedcaldav.bind import doBind
</span><span class="cx"> from twistedcaldav.cache import CacheStoreNotifierFactory
</span><ins>+from twistedcaldav.controlapi import ControlAPIResource
</ins><span class="cx"> from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
</span><span class="cx"> from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
</span><span class="cx"> from twistedcaldav.directory.digest import QopDigestCredentialFactory
</span><span class="lines">@@ -380,6 +381,7 @@
</span><span class="cx"> directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
</span><span class="cx"> apnSubscriptionResourceClass = APNSubscriptionResource
</span><span class="cx"> principalResourceClass = DirectoryPrincipalProvisioningResource
</span><ins>+ controlResourceClass = ControlAPIResource
</ins><span class="cx">
</span><span class="cx"> directory = newStore.directoryService()
</span><span class="cx"> principalCollection = principalResourceClass("/principals/", directory)
</span><span class="lines">@@ -669,6 +671,19 @@
</span><span class="cx"> root.putChild("admin", webAdmin)
</span><span class="cx">
</span><span class="cx"> #
</span><ins>+ # Control API
+ #
+ if config.EnableControlAPI:
+ log.info("Setting up Control API resource")
+ controlAPI = controlResourceClass(
+ root,
+ directory,
+ newStore,
+ principalCollections=(principalCollection,),
+ )
+ root.putChild("control", controlAPI)
+
+ #
</ins><span class="cx"> # Apple Push Notification Subscriptions
</span><span class="cx"> #
</span><span class="cx"> apnConfig = config.Notifications.Services.APNS
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolsprincipalspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/principals.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/principals.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/calendarserver/tools/principals.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -133,8 +133,9 @@
</span><span class="cx"> "remove",
</span><span class="cx"> "search=",
</span><span class="cx"> "list-principal-types",
</span><del>- "print-group-info",
</del><span class="cx"> "list-principals=",
</span><ins>+
+ # Proxies
</ins><span class="cx"> "list-read-proxies",
</span><span class="cx"> "list-write-proxies",
</span><span class="cx"> "list-proxies",
</span><span class="lines">@@ -142,17 +143,27 @@
</span><span class="cx"> "add-read-proxy=",
</span><span class="cx"> "add-write-proxy=",
</span><span class="cx"> "remove-proxy=",
</span><ins>+
+ # Groups
+ "list-group-members",
+ "add-group-member=",
+ "remove-group-member=",
+ "print-group-info",
+ "refresh-groups",
+
+ # Scheduling
</ins><span class="cx"> "set-auto-schedule-mode=",
</span><span class="cx"> "get-auto-schedule-mode",
</span><span class="cx"> "set-auto-accept-group=",
</span><span class="cx"> "get-auto-accept-group",
</span><ins>+
+ # Principal details
</ins><span class="cx"> "set-geo=",
</span><span class="cx"> "get-geo",
</span><span class="cx"> "set-address=",
</span><span class="cx"> "get-address",
</span><span class="cx"> "set-street-address=",
</span><span class="cx"> "get-street-address",
</span><del>- "refresh-groups",
</del><span class="cx"> "verbose",
</span><span class="cx"> ],
</span><span class="cx"> )
</span><span class="lines">@@ -195,12 +206,6 @@
</span><span class="cx"> elif opt in ("", "--list-principal-types"):
</span><span class="cx"> listPrincipalTypes = True
</span><span class="cx">
</span><del>- elif opt in ("", "--print-group-info"):
- printGroupInfo = True
-
- elif opt in ("", "--refresh-groups"):
- scheduleGroupRefresh = True
-
</del><span class="cx"> elif opt in ("", "--list-principals"):
</span><span class="cx"> listPrincipals = arg
</span><span class="cx">
</span><span class="lines">@@ -231,6 +236,21 @@
</span><span class="cx"> elif opt in ("", "--remove-proxy"):
</span><span class="cx"> principalActions.append((action_removeProxy, arg))
</span><span class="cx">
</span><ins>+ elif opt in ("", "--list-group-members"):
+ principalActions.append((action_listGroupMembers,))
+
+ elif opt in ("--add-group-member"):
+ principalActions.append((action_addGroupMember, arg))
+
+ elif opt in ("", "--remove-group-member"):
+ principalActions.append((action_removeGroupMember, arg))
+
+ elif opt in ("", "--print-group-info"):
+ printGroupInfo = True
+
+ elif opt in ("", "--refresh-groups"):
+ scheduleGroupRefresh = True
+
</ins><span class="cx"> elif opt in ("", "--set-auto-schedule-mode"):
</span><span class="cx"> try:
</span><span class="cx"> if arg not in allowedAutoScheduleModes:
</span><span class="lines">@@ -658,6 +678,128 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><ins>+@inlineCallbacks
+def action_listGroupMembers(store, record):
+ members = yield record.members()
+ if members:
+ print("Group members for %s:\n" % (
+ prettyRecord(record)
+ ))
+ printRecordList(members)
+ print("")
+ else:
+ print("No group members for %s" % (prettyRecord(record),))
+
+
+
+@inlineCallbacks
+def action_addGroupMember(store, record, *memberIDs):
+ directory = store.directoryService()
+ existingMembers = yield record.members()
+ existingMemberUIDs = set([member.uid for member in existingMembers])
+ add = set()
+ for memberID in memberIDs:
+ memberRecord = yield recordForPrincipalID(directory, memberID)
+ if memberRecord is None:
+ print("Invalid member ID: %s" % (memberID,))
+ elif memberRecord.uid in existingMemberUIDs:
+ print("Existing member ID: %s" % (memberID,))
+ else:
+ add.add(memberRecord)
+
+ if add:
+ yield record.addMembers(add)
+ for memberRecord in add:
+ print(
+ "Added {member} for {record}".format(
+ member=prettyRecord(memberRecord),
+ record=prettyRecord(record)
+ )
+ )
+ yield record.service.updateRecords([record], create=False)
+
+
+
+@inlineCallbacks
+def action_removeGroupMember(store, record, *memberIDs):
+ directory = store.directoryService()
+ existingMembers = yield record.members()
+ existingMemberUIDs = set([member.uid for member in existingMembers])
+ remove = set()
+ for memberID in memberIDs:
+ memberRecord = yield recordForPrincipalID(directory, memberID)
+ if memberRecord is None:
+ print("Invalid member ID: %s" % (memberID,))
+ elif memberRecord.uid not in existingMemberUIDs:
+ print("Missing member ID: %s" % (memberID,))
+ else:
+ remove.add(memberRecord)
+
+ if remove:
+ yield record.removeMembers(remove)
+ for memberRecord in remove:
+ print(
+ "Removed {member} for {record}".format(
+ member=prettyRecord(memberRecord),
+ record=prettyRecord(record)
+ )
+ )
+ yield record.service.updateRecords([record], create=False)
+
+
+
+@inlineCallbacks
+def printGroupCacherInfo(service, store):
+ """
+ Print all groups that have been delegated to, their cached members, and
+ who delegated to those groups.
+ """
+ directory = store.directoryService()
+ txn = store.newTransaction()
+ groupUIDs = yield txn.allGroupDelegates()
+ for groupUID in groupUIDs:
+ (
+ groupID, name, _ignore_membershipHash, modified, _ignore_extant
+ ) = yield txn.groupByUID(
+ groupUID
+ )
+ print("Group: \"{name}\" ({uid})".format(name=name, uid=groupUID))
+
+ for txt, readWrite in (("read-only", False), ("read-write", True)):
+ delegatorUIDs = yield txn.delegatorsToGroup(groupID, readWrite)
+ for delegatorUID in delegatorUIDs:
+ delegator = yield directory.recordWithUID(delegatorUID)
+ print(
+ "...has {rw} access to {rec}".format(
+ rw=txt, rec=prettyRecord(delegator)
+ )
+ )
+
+ print("Group members:")
+ memberUIDs = yield txn.groupMemberUIDs(groupID)
+ for memberUID in memberUIDs:
+ record = yield directory.recordWithUID(memberUID)
+ print(prettyRecord(record))
+
+ print("Last cached: {} GMT".format(modified))
+ print()
+
+ yield txn.commit()
+
+
+
+@inlineCallbacks
+def scheduleGroupRefreshJob(service, store):
+ """
+ Schedule GroupCacherPollingWork
+ """
+ txn = store.newTransaction()
+ print("Scheduling a group refresh")
+ yield GroupCacherPollingWork.reschedule(txn, 0, force=True)
+ yield txn.commit()
+
+
+
</ins><span class="cx"> def action_getAutoScheduleMode(store, record):
</span><span class="cx"> print(
</span><span class="cx"> "Auto-schedule mode for {record} is {mode}".format(
</span><span class="lines">@@ -812,58 +954,6 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><del>-@inlineCallbacks
-def printGroupCacherInfo(service, store):
- """
- Print all groups that have been delegated to, their cached members, and
- who delegated to those groups.
- """
- directory = store.directoryService()
- txn = store.newTransaction()
- groupUIDs = yield txn.allGroupDelegates()
- for groupUID in groupUIDs:
- (
- groupID, name, _ignore_membershipHash, modified, _ignore_extant
- ) = yield txn.groupByUID(
- groupUID
- )
- print("Group: \"{name}\" ({uid})".format(name=name, uid=groupUID))
-
- for txt, readWrite in (("read-only", False), ("read-write", True)):
- delegatorUIDs = yield txn.delegatorsToGroup(groupID, readWrite)
- for delegatorUID in delegatorUIDs:
- delegator = yield directory.recordWithUID(delegatorUID)
- print(
- "...has {rw} access to {rec}".format(
- rw=txt, rec=prettyRecord(delegator)
- )
- )
-
- print("Group members:")
- memberUIDs = yield txn.groupMemberUIDs(groupID)
- for memberUID in memberUIDs:
- record = yield directory.recordWithUID(memberUID)
- print(prettyRecord(record))
-
- print("Last cached: {} GMT".format(modified))
- print()
-
- yield txn.commit()
-
-
-
-@inlineCallbacks
-def scheduleGroupRefreshJob(service, store):
- """
- Schedule GroupCacherPollingWork
- """
- txn = store.newTransaction()
- print("Scheduling a group refresh")
- yield GroupCacherPollingWork.reschedule(txn, 0, force=True)
- yield txn.commit()
-
-
-
</del><span class="cx"> def abort(msg, status=1):
</span><span class="cx"> sys.stdout.write("%s\n" % (msg,))
</span><span class="cx"> try:
</span></span></pre></div>
<a id="CalendarServertrunkcalendarservertoolstesttest_principalspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/calendarserver/tools/test/test_principals.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/calendarserver/tools/test/test_principals.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/calendarserver/tools/test/test_principals.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -293,7 +293,66 @@
</span><span class="cx"> self.fail("Expected command failure")
</span><span class="cx">
</span><span class="cx">
</span><ins>+ @inlineCallbacks
+ def test_groupChanges(self):
+ results = yield self.runCommand(
+ "--list-group-members", "groups:testgroup1"
+ )
+ self.assertTrue("user01" in results)
+ self.assertTrue("user02" in results)
+ self.assertTrue("user03" not in results)
</ins><span class="cx">
</span><ins>+ results = yield self.runCommand(
+ "--add-group-member", "users:user03", "groups:testgroup1"
+ )
+ self.assertTrue("Added" in results)
+ self.assertTrue("Existing" not in results)
+ self.assertTrue("Invalid" not in results)
+
+ results = yield self.runCommand(
+ "--list-group-members", "groups:testgroup1"
+ )
+ self.assertTrue("user01" in results)
+ self.assertTrue("user02" in results)
+ self.assertTrue("user03" in results)
+
+ results = yield self.runCommand(
+ "--add-group-member", "users:user03", "groups:testgroup1"
+ )
+ self.assertTrue("Added" not in results)
+ self.assertTrue("Existing" in results)
+ self.assertTrue("Invalid" not in results)
+
+ results = yield self.runCommand(
+ "--add-group-member", "users:bogus", "groups:testgroup1"
+ )
+ self.assertTrue("Added" not in results)
+ self.assertTrue("Existing" not in results)
+ self.assertTrue("Invalid" in results)
+
+ results = yield self.runCommand(
+ "--remove-group-member", "users:user03", "groups:testgroup1"
+ )
+ self.assertTrue("Removed" in results)
+ self.assertTrue("Missing" not in results)
+ self.assertTrue("Invalid" not in results)
+
+ results = yield self.runCommand(
+ "--list-group-members", "groups:testgroup1"
+ )
+ self.assertTrue("user01" in results)
+ self.assertTrue("user02" in results)
+ self.assertTrue("user03" not in results)
+
+ results = yield self.runCommand(
+ "--remove-group-member", "users:user03", "groups:testgroup1"
+ )
+ self.assertTrue("Removed" not in results)
+ self.assertTrue("Missing" in results)
+ self.assertTrue("Invalid" not in results)
+
+
+
</ins><span class="cx"> class SetProxiesTestCase(StoreTestCase):
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span></span></pre></div>
<a id="CalendarServertrunkconfcaldavdtestplist"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/conf/caldavd-test.plist (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/conf/caldavd-test.plist        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/conf/caldavd-test.plist        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -823,6 +823,10 @@
</span><span class="cx"> <key>EnableWebAdmin</key>
</span><span class="cx"> <true/>
</span><span class="cx">
</span><ins>+ <!-- JSON Control API - only for testing -->
+        <key>EnableControlAPI</key>
+        <true/>
+
</ins><span class="cx"> <!-- Support for Content-Encoding compression -->
</span><span class="cx"> <key>ResponseCompression</key>
</span><span class="cx"> <false/> <!-- Off for testing, as debugging is easier that way. -->
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavcontrolapipy"></a>
<div class="addfile"><h4>Added: CalendarServer/trunk/twistedcaldav/controlapi.py (0 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/controlapi.py         (rev 0)
+++ CalendarServer/trunk/twistedcaldav/controlapi.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -0,0 +1,337 @@
</span><ins>+##
+# 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.
+##
+
+"""
+Timezone service resource and operations.
+
+This is based on http://tools.ietf.org/html/draft-douglass-timezone-service which is the CalConnect
+proposal for a standard timezone service.
+"""
+
+__all__ = [
+ "TimezoneStdServiceResource",
+]
+
+from calendarserver.tools.util import recordForPrincipalID
+
+from twext.enterprise.jobqueue import JobItem
+from twext.python.log import Logger
+
+from twisted.internet import reactor
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+
+from twistedcaldav.config import config
+from twistedcaldav.extensions import DAVResource, \
+ DAVResourceWithoutChildrenMixin
+from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
+
+from txdav.who.groups import GroupCacherPollingWork, GroupRefreshWork, \
+ GroupAttendeeReconciliationWork, GroupDelegateChangesWork, \
+ GroupShareeReconciliationWork
+from txdav.xml import element as davxml
+
+from txweb2 import responsecode
+from txweb2.dav.method.propfind import http_PROPFIND
+from txweb2.dav.noneprops import NonePropertyStore
+from txweb2.dav.util import allDataFromStream
+from txweb2.http import HTTPError, JSONResponse, StatusResponse
+from txweb2.http import Response
+from txweb2.http_headers import MimeType
+
+import json
+
+log = Logger()
+
+class ControlAPIResource (ReadOnlyNoCopyResourceMixIn, DAVResourceWithoutChildrenMixin, DAVResource):
+ """
+ Resource used to execute admin commands.
+
+ Extends L{DAVResource} to provide service functionality.
+ """
+
+ def __init__(self, root, directory, store, principalCollections=()):
+ """
+ @param parent: the parent resource of this one.
+ """
+
+ DAVResource.__init__(self, principalCollections=principalCollections)
+
+ self.parent = root
+ self._store = store
+ self._directory = directory
+
+
+ def deadProperties(self):
+ if not hasattr(self, "_dead_properties"):
+ self._dead_properties = NonePropertyStore(self)
+ return self._dead_properties
+
+
+ def etag(self):
+ return succeed(None)
+
+
+ def checkPreconditions(self, request):
+ return None
+
+
+ def defaultAccessControlList(self):
+ return succeed(davxml.ACL(*config.AdminACEs))
+
+
+ def contentType(self):
+ return MimeType.fromString("text/html; charset=utf-8")
+
+
+ def resourceType(self):
+ return None
+
+
+ def isCollection(self):
+ return False
+
+
+ def isCalendarCollection(self):
+ return False
+
+
+ def isPseudoCalendarCollection(self):
+ return False
+
+
+ def render(self, request):
+ output = """<html>
+<head>
+<title>Control API Resource</title>
+</head>
+<body>
+<h1>Control API Resource.</h1>
+</body
+</html>"""
+
+ response = Response(200, {}, output)
+ response.headers.setHeader("content-type", MimeType("text", "html"))
+ return response
+
+ http_PROPFIND = http_PROPFIND
+
+ def http_GET(self, request):
+ """
+ GET just returns HTML description.
+ """
+ return self.render(request)
+
+
+ def _ok(self, status, description, result=None):
+ if result is None:
+ result = {}
+ result["status"] = status
+ result["description"] = description
+ return JSONResponse(
+ responsecode.OK,
+ result,
+ )
+
+
+ def _error(self, status, description):
+ raise HTTPError(JSONResponse(
+ responsecode.BAD_REQUEST,
+ {
+ "status": status,
+ "description": description,
+ },
+ ))
+
+
+ def _recordsToJSON(self, records):
+ results = []
+ for record in sorted(records, key=lambda r: r.uid):
+ try:
+ shortNames = record.shortNames
+ except AttributeError:
+ shortNames = []
+ results.append(
+ {
+ "type": record.recordType.name,
+ "cn": record.displayName,
+ "uid": record.uid,
+ "sn": shortNames
+ }
+ )
+ return results
+
+
+ @inlineCallbacks
+ def http_POST(self, request):
+ """
+ POST method with JSON body is used for control.
+ """
+
+ #
+ # Check authentication and access controls
+ #
+ yield self.authorize(request, (davxml.Read(),))
+
+ contentType = request.headers.getHeader("content-type")
+ # Check content first
+ if "{}/{}".format(contentType.mediaType, contentType.mediaSubtype) != "application/json":
+ self.log.error("MIME type {mime} not allowed in request", mime=contentType)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "MIME type {} not allowed in request".format(contentType)))
+
+ body = (yield allDataFromStream(request.stream))
+ try:
+ j = json.loads(body)
+ except (ValueError, TypeError) as e:
+ self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
+ raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
+
+ try:
+ action = j["action"]
+ except KeyError:
+ self._error("error", "No 'action' member in root JSON object.")
+
+ method = "action_{}".format(action)
+ if not hasattr(self, method):
+ self._error("error", "The action '{}' is not supported.".format(action))
+
+ result = yield getattr(self, method)(j)
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def action_listgroupmembers(self, j):
+ try:
+ grpID = j["group"]
+ except KeyError:
+ self._error("error", "No 'group' member in root JSON object.")
+
+ try:
+ record = yield recordForPrincipalID(self._directory, grpID)
+ except ValueError:
+ record = None
+ if record is None:
+ self._error("error", "No group with id '{}' in the directory.".format(grpID))
+
+ members = yield record.members()
+
+ returnValue(self._ok("ok", "Group membership", {
+ "group": grpID,
+ "members": self._recordsToJSON(members),
+ }))
+
+
+ @inlineCallbacks
+ def action_addgroupmembers(self, j):
+ try:
+ grpID = j["group"]
+ except KeyError:
+ self._error("error", "No 'group' member in root JSON object.")
+ try:
+ memberIDs = j["members"]
+ except KeyError:
+ self._error("error", "No 'members' member in root JSON object.")
+
+ try:
+ record = yield recordForPrincipalID(self._directory, grpID)
+ except ValueError:
+ record = None
+ if record is None:
+ self._error("error", "No group with id '{}' in the directory.".format(grpID))
+
+ existingMembers = yield record.members()
+ existingMemberUIDs = set([member.uid for member in existingMembers])
+ add = set()
+ invalid = set()
+ exists = set()
+ for memberID in memberIDs:
+ memberRecord = yield recordForPrincipalID(self._directory, memberID)
+ if memberRecord is None:
+ invalid.add(memberID)
+ elif memberRecord.uid in existingMemberUIDs:
+ exists.add(memberRecord)
+ else:
+ add.add(memberRecord)
+
+ if add:
+ yield record.addMembers(add)
+ yield record.service.updateRecords([record], create=False)
+
+ returnValue(self._ok("ok", "Added group members", {
+ "group": grpID,
+ "added": self._recordsToJSON(add),
+ "exists": self._recordsToJSON(exists),
+ "invalid": sorted(invalid),
+ }))
+
+
+ @inlineCallbacks
+ def action_removegroupmembers(self, j):
+ try:
+ grpID = j["group"]
+ except KeyError:
+ self._error("error", "No 'group' member in root JSON object.")
+ try:
+ memberIDs = j["members"]
+ except KeyError:
+ self._error("error", "No 'members' member in root JSON object.")
+
+ try:
+ record = yield recordForPrincipalID(self._directory, grpID)
+ except ValueError:
+ record = None
+ if record is None:
+ self._error("error", "No group with id '{}' in the directory.".format(grpID))
+
+ existingMembers = yield record.members()
+ existingMemberUIDs = set([member.uid for member in existingMembers])
+ remove = set()
+ invalid = set()
+ missing = set()
+ for memberID in memberIDs:
+ memberRecord = yield recordForPrincipalID(self._directory, memberID)
+ if memberRecord is None:
+ invalid.add(memberID)
+ elif memberRecord.uid not in existingMemberUIDs:
+ missing.add(memberRecord)
+ else:
+ remove.add(memberRecord)
+
+ if remove:
+ record.removeMembers(remove)
+ yield record.service.updateRecords([record], create=False)
+
+ returnValue(self._ok("ok", "Removed group members", {
+ "group": grpID,
+ "removed": self._recordsToJSON(remove),
+ "missing": self._recordsToJSON(missing),
+ "invalid": sorted(invalid),
+ }))
+
+
+ @inlineCallbacks
+ def action_refreshgroups(self, j):
+ txn = self._store.newTransaction()
+ wp = yield GroupCacherPollingWork.reschedule(txn, 0, force=True)
+ jobID = wp.workItem.jobID
+ yield txn.commit()
+
+ if "wait" in j and j["wait"]:
+ yield JobItem.waitJobDone(self._store.newTransaction, reactor, 60.0, jobID)
+ yield JobItem.waitWorkDone(self._store.newTransaction, reactor, 60.0, (
+ GroupRefreshWork, GroupAttendeeReconciliationWork, GroupDelegateChangesWork, GroupShareeReconciliationWork,
+ ))
+
+ returnValue(self._ok("ok", "Group refresh scheduled"))
</ins></span></pre></div>
<a id="CalendarServertrunktwistedcaldavdirectoryaugmentpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/directory/augment.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/directory/augment.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/twistedcaldav/directory/augment.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -469,13 +469,11 @@
</span><span class="cx"> def _updateRecordInXMLDB(self, record, recordNode):
</span><span class="cx"> del recordNode[:]
</span><span class="cx"> addSubElement(recordNode, xmlaugmentsparser.ELEMENT_UID, record.uid)
</span><del>- addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLE, "true" if record.enabled else "false")
</del><span class="cx"> if record.serverID:
</span><span class="cx"> addSubElement(recordNode, xmlaugmentsparser.ELEMENT_SERVERID, record.serverID)
</span><span class="cx"> addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if record.enabledForCalendaring else "false")
</span><span class="cx"> addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLEADDRESSBOOK, "true" if record.enabledForAddressBooks else "false")
</span><span class="cx"> addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLELOGIN, "true" if record.enabledForLogin else "false")
</span><del>- addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if record.autoSchedule else "false")
</del><span class="cx"> if record.autoScheduleMode:
</span><span class="cx"> addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE_MODE, record.autoScheduleMode)
</span><span class="cx"> if record.autoAcceptGroup:
</span></span></pre></div>
<a id="CalendarServertrunktwistedcaldavstdconfigpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -621,6 +621,11 @@
</span><span class="cx"> "EnableWebAdmin" : True,
</span><span class="cx">
</span><span class="cx"> #
</span><ins>+ # JSON control API - only for testing
+ #
+ "EnableControlAPI" : False,
+
+ #
</ins><span class="cx"> # Scheduling related options
</span><span class="cx"> #
</span><span class="cx">
</span></span></pre></div>
<a id="CalendarServertrunktxdavdpsclientpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/client.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/client.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/txdav/dps/client.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -42,7 +42,9 @@
</span><span class="cx"> MembersCommand, GroupsCommand, SetMembersCommand,
</span><span class="cx"> VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
</span><span class="cx"> WikiAccessForUIDCommand, ContinuationCommand,
</span><del>- StatsCommand, ExternalDelegatesCommand, ExpandedMemberUIDsCommand
</del><ins>+ StatsCommand, ExternalDelegatesCommand, ExpandedMemberUIDsCommand,
+ AddMembersCommand, RemoveMembersCommand,
+ UpdateRecordsCommand
</ins><span class="cx"> )
</span><span class="cx"> from txdav.who.delegates import RecordType as DelegatesRecordType
</span><span class="cx"> from txdav.who.directory import (
</span><span class="lines">@@ -407,6 +409,19 @@
</span><span class="cx"> )
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def updateRecords(self, records, create=False):
+ # FIXME: cannot create right now
+ if create:
+ raise ValueError("Cannot create records using DPS")
+
+ recordUIDs = [r.uid.encode("utf-8") for r in records]
+ return self._sendCommand(
+ UpdateRecordsCommand,
+ uids=recordUIDs,
+ create=False,
+ )
+
+
</ins><span class="cx"> @inlineCallbacks
</span><span class="cx"> def stats(self):
</span><span class="cx"> try:
</span><span class="lines">@@ -476,6 +491,28 @@
</span><span class="cx"> )
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def addMembers(self, members):
+ log.debug("DPS Client addMembers")
+ memberUIDs = [m.uid.encode("utf-8") for m in members]
+ return self.service._call(
+ AddMembersCommand,
+ lambda x: x['success'],
+ uid=self.uid.encode("utf-8"),
+ memberUIDs=memberUIDs
+ )
+
+
+ def removeMembers(self, members):
+ log.debug("DPS Client removeMembers")
+ memberUIDs = [m.uid.encode("utf-8") for m in members]
+ return self.service._call(
+ RemoveMembersCommand,
+ lambda x: x['success'],
+ uid=self.uid.encode("utf-8"),
+ memberUIDs=memberUIDs
+ )
+
+
</ins><span class="cx"> def setMembers(self, members):
</span><span class="cx"> log.debug("DPS Client setMembers")
</span><span class="cx"> memberUIDs = [m.uid.encode("utf-8") for m in members]
</span></span></pre></div>
<a id="CalendarServertrunktxdavdpscommandspy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/commands.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/commands.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/txdav/dps/commands.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -119,7 +119,7 @@
</span><span class="cx">
</span><span class="cx"> class UpdateRecordsCommand(amp.Command):
</span><span class="cx"> arguments = [
</span><del>- ('items', amp.ListOf(amp.String())),
</del><ins>+ ('uids', amp.ListOf(amp.String())),
</ins><span class="cx"> ('create', amp.Boolean(optional=True)),
</span><span class="cx"> ]
</span><span class="cx"> response = [
</span><span class="lines">@@ -160,6 +160,28 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx">
</span><ins>+class AddMembersCommand(amp.Command):
+ arguments = [
+ ('uid', amp.String()),
+ ('memberUIDs', amp.ListOf(amp.String())),
+ ]
+ response = [
+ ('success', amp.Boolean()),
+ ]
+
+
+
+class RemoveMembersCommand(amp.Command):
+ arguments = [
+ ('uid', amp.String()),
+ ('memberUIDs', amp.ListOf(amp.String())),
+ ]
+ response = [
+ ('success', amp.Boolean()),
+ ]
+
+
+
</ins><span class="cx"> class SetMembersCommand(amp.Command):
</span><span class="cx"> arguments = [
</span><span class="cx"> ('uid', amp.String()),
</span></span></pre></div>
<a id="CalendarServertrunktxdavdpsserverpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/server.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/server.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/txdav/dps/server.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -38,8 +38,9 @@
</span><span class="cx"> MembersCommand, GroupsCommand, SetMembersCommand,
</span><span class="cx"> VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
</span><span class="cx"> WikiAccessForUIDCommand, ContinuationCommand,
</span><del>- ExternalDelegatesCommand, StatsCommand, ExpandedMemberUIDsCommand
- # UpdateRecordsCommand, RemoveRecordsCommand
</del><ins>+ ExternalDelegatesCommand, StatsCommand, ExpandedMemberUIDsCommand,
+ AddMembersCommand, RemoveMembersCommand,
+ UpdateRecordsCommand, # RemoveRecordsCommand,
</ins><span class="cx"> )
</span><span class="cx"> from txdav.who.cache import CachingDirectoryService
</span><span class="cx"> from txdav.who.util import directoryFromConfig
</span><span class="lines">@@ -396,6 +397,66 @@
</span><span class="cx"> returnValue(response)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ @AddMembersCommand.responder
+ @inlineCallbacks
+ def addMembers(self, uid, memberUIDs):
+ uid = uid.decode("utf-8")
+ memberUIDs = [m.decode("utf-8") for m in memberUIDs]
+ log.debug("Add Members: {u} -> {m}", u=uid, m=memberUIDs)
+ try:
+ record = (yield self._directory.recordWithUID(uid))
+ except Exception as e:
+ log.error("Failed in addMembers", 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.addMembers(memberRecords)
+ success = True
+ else:
+ success = False
+
+ response = {
+ "success": success,
+ }
+ # log.debug("Responding with: {response}", response=response)
+ returnValue(response)
+
+
+ @RemoveMembersCommand.responder
+ @inlineCallbacks
+ def removeMembers(self, uid, memberUIDs):
+ uid = uid.decode("utf-8")
+ memberUIDs = [m.decode("utf-8") for m in memberUIDs]
+ log.debug("Remove Members: {u} -> {m}", u=uid, m=memberUIDs)
+ try:
+ record = (yield self._directory.recordWithUID(uid))
+ except Exception as e:
+ log.error("Failed in removeMembers", 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.removeMembers(memberRecords)
+ success = True
+ else:
+ success = False
+
+ response = {
+ "success": success,
+ }
+ # log.debug("Responding with: {response}", response=response)
+ returnValue(response)
+
+
</ins><span class="cx"> @SetMembersCommand.responder
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def setMembers(self, uid, memberUIDs):
</span><span class="lines">@@ -426,6 +487,31 @@
</span><span class="cx"> returnValue(response)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ @UpdateRecordsCommand.responder
+ @inlineCallbacks
+ def updateRecords(self, uids, create):
+ # FIXME: does not support creation right now
+ if not create:
+ recordUIDs = [m.decode("utf-8") for m in uids]
+ log.debug("Update Records: {u} create:{c}", u=recordUIDs, c=create)
+ records = []
+ for uid in recordUIDs:
+ record = yield self._directory.recordWithUID(uid)
+ if record is not None:
+ records.append(record)
+ yield self._directory.updateRecords(records, create=False)
+ success = True
+ else:
+ log.debug("Update Records - cannot create")
+ success = False
+
+ response = {
+ "success": success,
+ }
+ # log.debug("Responding with: {response}", response=response)
+ returnValue(response)
+
+
</ins><span class="cx"> @GroupsCommand.responder
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def groups(self, uid):
</span></span></pre></div>
<a id="CalendarServertrunktxdavdpstesttest_clientpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/dps/test/test_client.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/dps/test/test_client.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/txdav/dps/test/test_client.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -81,8 +81,11 @@
</span><span class="cx">
</span><span class="cx"> # The "remote" directory service
</span><span class="cx"> if testMode == "xml":
</span><del>- path = os.path.join(os.path.dirname(__file__), "test.xml")
- remoteDirectory = CalendarXMLDirectoryService(FilePath(path))
</del><ins>+ # Need a copy as it might change
+ path = FilePath(os.path.join(os.path.dirname(__file__), "test.xml"))
+ copy = FilePath(self.mktemp())
+ path.copyTo(copy)
+ remoteDirectory = CalendarXMLDirectoryService(copy)
</ins><span class="cx"> elif testMode == "od":
</span><span class="cx"> remoteDirectory = CalendarODDirectoryService()
</span><span class="cx">
</span><span class="lines">@@ -95,16 +98,16 @@
</span><span class="cx"> # actual networking
</span><span class="cx"> self.patch(self.directory, "_getConnection", lambda: succeed(client))
</span><span class="cx">
</span><del>- # Wrap the normal _call method with one that flushes the IOPump
</del><ins>+ # Wrap the normal _sendCommand method with one that flushes the IOPump
</ins><span class="cx"> # afterwards
</span><del>- origCall = self.directory._call
</del><ins>+ origCall = self.directory._sendCommand
</ins><span class="cx">
</span><span class="cx"> def newCall(*args, **kwds):
</span><span class="cx"> d = origCall(*args, **kwds)
</span><span class="cx"> pump.flush()
</span><span class="cx"> return d
</span><span class="cx">
</span><del>- self.patch(self.directory, "_call", newCall)
</del><ins>+ self.patch(self.directory, "_sendCommand", newCall)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><span class="lines">@@ -286,6 +289,53 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def test_group_changes(self):
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ members = yield group.members()
+ self.assertEquals(len(members), 5)
+
+ # Add new member
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ user = yield self.directory.recordWithUID(u"__cdaboo__")
+ yield group.addMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ members = yield group.members()
+ self.assertEquals(len(members), 6)
+
+ # Add existing member
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ user = yield self.directory.recordWithUID(u"__wsanchez__")
+ yield group.addMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ members = yield group.members()
+ self.assertEquals(len(members), 6)
+
+ # Remove existing member
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ user = yield self.directory.recordWithUID(u"__cdaboo__")
+ yield group.removeMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ members = yield group.members()
+ self.assertEquals(len(members), 5)
+
+ # Remove missing member
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ user = yield self.directory.recordWithUID(u"__cdaboo__")
+ yield group.removeMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__twisted__")
+ members = yield group.members()
+ self.assertEquals(len(members), 5)
+
+
+ @inlineCallbacks
</ins><span class="cx"> def test_verifyPlaintextPassword(self):
</span><span class="cx"> expectations = (
</span><span class="cx"> (testPassword, True), # Correct
</span><span class="lines">@@ -369,16 +419,16 @@
</span><span class="cx"> # actual networking
</span><span class="cx"> self.patch(self.client, "_getConnection", lambda: succeed(client))
</span><span class="cx">
</span><del>- # Wrap the normal _call method with one that flushes the IOPump
</del><ins>+ # Wrap the normal _sendCommand method with one that flushes the IOPump
</ins><span class="cx"> # afterwards
</span><del>- origCall = self.client._call
</del><ins>+ origCall = self.client._sendCommand
</ins><span class="cx">
</span><span class="cx"> def newCall(*args, **kwds):
</span><span class="cx"> d = origCall(*args, **kwds)
</span><span class="cx"> pump.flush()
</span><span class="cx"> return d
</span><span class="cx">
</span><del>- self.patch(self.client, "_call", newCall)
</del><ins>+ self.patch(self.client, "_sendCommand", newCall)
</ins><span class="cx">
</span><span class="cx">
</span><span class="cx"> def configure(self):
</span><span class="lines">@@ -620,6 +670,53 @@
</span><span class="cx">
</span><span class="cx">
</span><span class="cx"> @inlineCallbacks
</span><ins>+ def test_group_changes(self):
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ members = yield group.members()
+ self.assertEquals(len(members), 3)
+
+ # Add new member
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ user = yield self.directory.recordWithUID(u"__cdaboo1__")
+ yield group.addMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ members = yield group.members()
+ self.assertEquals(len(members), 4)
+
+ # Add existing member
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ user = yield self.directory.recordWithUID(u"__wsanchez1__")
+ yield group.addMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ members = yield group.members()
+ self.assertEquals(len(members), 4)
+
+ # Remove existing member
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ user = yield self.directory.recordWithUID(u"__cdaboo1__")
+ yield group.removeMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ members = yield group.members()
+ self.assertEquals(len(members), 3)
+
+ # Remove missing member
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ user = yield self.directory.recordWithUID(u"__cdaboo1__")
+ yield group.removeMembers((user,))
+ yield self.directory.updateRecords((group,), False)
+
+ group = yield self.directory.recordWithUID(u"__top_group_1__")
+ members = yield group.members()
+ self.assertEquals(len(members), 3)
+
+
+ @inlineCallbacks
</ins><span class="cx"> def test_verifyPlaintextPassword(self):
</span><span class="cx"> expectations = (
</span><span class="cx"> (u"zehcnasw", True), # Correct
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhoaugmentpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/augment.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/augment.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/txdav/who/augment.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -294,7 +294,8 @@
</span><span class="cx"> # Split out the base fields from the augment fields
</span><span class="cx"> baseFields, augmentFields = self._splitFields(record)
</span><span class="cx">
</span><del>- if augmentFields:
</del><ins>+ # Ignore groups for now
+ if augmentFields and record.recordType != RecordType.group:
</ins><span class="cx"> # Create an AugmentRecord
</span><span class="cx"> autoScheduleMode = {
</span><span class="cx"> AutoScheduleMode.none: "none",
</span><span class="lines">@@ -324,7 +325,7 @@
</span><span class="cx"> augmentRecords.append(augmentRecord)
</span><span class="cx">
</span><span class="cx"> # Create new base records:
</span><del>- baseRecords.append(DirectoryRecord(self._directory, baseFields))
</del><ins>+ baseRecords.append(DirectoryRecord(self._directory, record._baseRecord.fields if hasattr(record, "_baseRecord") else baseFields))
</ins><span class="cx">
</span><span class="cx"> # Apply the augment records
</span><span class="cx"> if augmentRecords:
</span><span class="lines">@@ -494,6 +495,18 @@
</span><span class="cx"> returnValue(augmented)
</span><span class="cx">
</span><span class="cx">
</span><ins>+ def addMembers(self, memberRecords):
+ return self._baseRecord.addMembers(memberRecords)
+
+
+ def removeMembers(self, memberRecords):
+ return self._baseRecord.removeMembers(memberRecords)
+
+
+ def setMembers(self, memberRecords):
+ return self._baseRecord.setMembers(memberRecords)
+
+
</ins><span class="cx"> @timed
</span><span class="cx"> @inlineCallbacks
</span><span class="cx"> def groups(self):
</span></span></pre></div>
<a id="CalendarServertrunktxdavwhoxmlpy"></a>
<div class="modfile"><h4>Modified: CalendarServer/trunk/txdav/who/xml.py (13987 => 13988)</h4>
<pre class="diff"><span>
<span class="info">--- CalendarServer/trunk/txdav/who/xml.py        2014-09-22 15:24:13 UTC (rev 13987)
+++ CalendarServer/trunk/txdav/who/xml.py        2014-09-22 17:00:48 UTC (rev 13988)
</span><span class="lines">@@ -186,3 +186,13 @@
</span><span class="cx"> recordTypeValue = ConstantsContainer(
</span><span class="cx"> (BaseDirectoryService.recordTypeValue, RecordTypeValue)
</span><span class="cx"> )
</span><ins>+
+
+ xmlFieldOrder = BaseDirectoryService.xmlFieldOrder + (
+ FieldName.capacity,
+ FieldName.floor,
+ FieldName.associatedAddress,
+ FieldName.abbreviatedName,
+ FieldName.streetAddress,
+ FieldName.geographicLocation,
+ )
</ins></span></pre>
</div>
</div>
</body>
</html>