[CalendarServer-changes] [13988] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Sep 22 10:00:48 PDT 2014


Revision: 13988
          http://trac.calendarserver.org//changeset/13988
Author:   cdaboo at apple.com
Date:     2014-09-22 10:00:48 -0700 (Mon, 22 Sep 2014)
Log Message:
-----------
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.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/calendarserver/tools/principals.py
    CalendarServer/trunk/calendarserver/tools/test/test_principals.py
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/twistedcaldav/directory/augment.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/txdav/dps/client.py
    CalendarServer/trunk/txdav/dps/commands.py
    CalendarServer/trunk/txdav/dps/server.py
    CalendarServer/trunk/txdav/dps/test/test_client.py
    CalendarServer/trunk/txdav/who/augment.py
    CalendarServer/trunk/txdav/who/xml.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/controlapi.py

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- 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)
@@ -61,6 +61,7 @@
 from twext.enterprise.ienterprise import POSTGRES_DIALECT
 from twistedcaldav.bind import doBind
 from twistedcaldav.cache import CacheStoreNotifierFactory
+from twistedcaldav.controlapi import ControlAPIResource
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
 from twistedcaldav.directory.digest import QopDigestCredentialFactory
@@ -380,6 +381,7 @@
     directoryBackedAddressBookResourceClass = DirectoryBackedAddressBookResource
     apnSubscriptionResourceClass = APNSubscriptionResource
     principalResourceClass = DirectoryPrincipalProvisioningResource
+    controlResourceClass = ControlAPIResource
 
     directory = newStore.directoryService()
     principalCollection = principalResourceClass("/principals/", directory)
@@ -669,6 +671,19 @@
         root.putChild("admin", webAdmin)
 
     #
+    # Control API
+    #
+    if config.EnableControlAPI:
+        log.info("Setting up Control API resource")
+        controlAPI = controlResourceClass(
+            root,
+            directory,
+            newStore,
+            principalCollections=(principalCollection,),
+        )
+        root.putChild("control", controlAPI)
+
+    #
     # Apple Push Notification Subscriptions
     #
     apnConfig = config.Notifications.Services.APNS

Modified: CalendarServer/trunk/calendarserver/tools/principals.py
===================================================================
--- 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)
@@ -133,8 +133,9 @@
                 "remove",
                 "search=",
                 "list-principal-types",
-                "print-group-info",
                 "list-principals=",
+
+                # Proxies
                 "list-read-proxies",
                 "list-write-proxies",
                 "list-proxies",
@@ -142,17 +143,27 @@
                 "add-read-proxy=",
                 "add-write-proxy=",
                 "remove-proxy=",
+
+                # Groups
+                "list-group-members",
+                "add-group-member=",
+                "remove-group-member=",
+                "print-group-info",
+                "refresh-groups",
+
+                # Scheduling
                 "set-auto-schedule-mode=",
                 "get-auto-schedule-mode",
                 "set-auto-accept-group=",
                 "get-auto-accept-group",
+
+                # Principal details
                 "set-geo=",
                 "get-geo",
                 "set-address=",
                 "get-address",
                 "set-street-address=",
                 "get-street-address",
-                "refresh-groups",
                 "verbose",
             ],
         )
@@ -195,12 +206,6 @@
         elif opt in ("", "--list-principal-types"):
             listPrincipalTypes = True
 
-        elif opt in ("", "--print-group-info"):
-            printGroupInfo = True
-
-        elif opt in ("", "--refresh-groups"):
-            scheduleGroupRefresh = True
-
         elif opt in ("", "--list-principals"):
             listPrincipals = arg
 
@@ -231,6 +236,21 @@
         elif opt in ("", "--remove-proxy"):
             principalActions.append((action_removeProxy, arg))
 
+        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
+
         elif opt in ("", "--set-auto-schedule-mode"):
             try:
                 if arg not in allowedAutoScheduleModes:
@@ -658,6 +678,128 @@
 
 
 
+ at 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),))
+
+
+
+ at 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)
+
+
+
+ at 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)
+
+
+
+ at 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()
+
+
+
+ at 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()
+
+
+
 def action_getAutoScheduleMode(store, record):
     print(
         "Auto-schedule mode for {record} is {mode}".format(
@@ -812,58 +954,6 @@
 
 
 
- at 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()
-
-
-
- at 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()
-
-
-
 def abort(msg, status=1):
     sys.stdout.write("%s\n" % (msg,))
     try:

Modified: CalendarServer/trunk/calendarserver/tools/test/test_principals.py
===================================================================
--- 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)
@@ -293,7 +293,66 @@
             self.fail("Expected command failure")
 
 
+    @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)
 
+        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)
+
+
+
 class SetProxiesTestCase(StoreTestCase):
 
     @inlineCallbacks

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- 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)
@@ -823,6 +823,10 @@
     <key>EnableWebAdmin</key>
     <true/>
 
+    <!-- JSON Control API - only for testing -->
+	<key>EnableControlAPI</key>
+	<true/>
+
     <!-- Support for Content-Encoding compression -->
     <key>ResponseCompression</key>
     <false/>  <!-- Off for testing, as debugging is easier that way. -->

Added: CalendarServer/trunk/twistedcaldav/controlapi.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/controlapi.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/controlapi.py	2014-09-22 17:00:48 UTC (rev 13988)
@@ -0,0 +1,337 @@
+##
+# 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"))

Modified: CalendarServer/trunk/twistedcaldav/directory/augment.py
===================================================================
--- 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)
@@ -469,13 +469,11 @@
     def _updateRecordInXMLDB(self, record, recordNode):
         del recordNode[:]
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_UID, record.uid)
-        addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLE, "true" if record.enabled else "false")
         if record.serverID:
             addSubElement(recordNode, xmlaugmentsparser.ELEMENT_SERVERID, record.serverID)
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLECALENDAR, "true" if record.enabledForCalendaring else "false")
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLEADDRESSBOOK, "true" if record.enabledForAddressBooks else "false")
         addSubElement(recordNode, xmlaugmentsparser.ELEMENT_ENABLELOGIN, "true" if record.enabledForLogin else "false")
-        addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE, "true" if record.autoSchedule else "false")
         if record.autoScheduleMode:
             addSubElement(recordNode, xmlaugmentsparser.ELEMENT_AUTOSCHEDULE_MODE, record.autoScheduleMode)
         if record.autoAcceptGroup:

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- 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)
@@ -621,6 +621,11 @@
     "EnableWebAdmin"          : True,
 
     #
+    # JSON control API - only for testing
+    #
+    "EnableControlAPI"        : False,
+
+    #
     # Scheduling related options
     #
 

Modified: CalendarServer/trunk/txdav/dps/client.py
===================================================================
--- 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)
@@ -42,7 +42,9 @@
     MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
     WikiAccessForUIDCommand, ContinuationCommand,
-    StatsCommand, ExternalDelegatesCommand, ExpandedMemberUIDsCommand
+    StatsCommand, ExternalDelegatesCommand, ExpandedMemberUIDsCommand,
+    AddMembersCommand, RemoveMembersCommand,
+    UpdateRecordsCommand
 )
 from txdav.who.delegates import RecordType as DelegatesRecordType
 from txdav.who.directory import (
@@ -407,6 +409,19 @@
         )
 
 
+    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,
+        )
+
+
     @inlineCallbacks
     def stats(self):
         try:
@@ -476,6 +491,28 @@
         )
 
 
+    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
+        )
+
+
     def setMembers(self, members):
         log.debug("DPS Client setMembers")
         memberUIDs = [m.uid.encode("utf-8") for m in members]

Modified: CalendarServer/trunk/txdav/dps/commands.py
===================================================================
--- 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)
@@ -119,7 +119,7 @@
 
 class UpdateRecordsCommand(amp.Command):
     arguments = [
-        ('items', amp.ListOf(amp.String())),
+        ('uids', amp.ListOf(amp.String())),
         ('create', amp.Boolean(optional=True)),
     ]
     response = [
@@ -160,6 +160,28 @@
 
 
 
+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()),
+    ]
+
+
+
 class SetMembersCommand(amp.Command):
     arguments = [
         ('uid', amp.String()),

Modified: CalendarServer/trunk/txdav/dps/server.py
===================================================================
--- 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)
@@ -38,8 +38,9 @@
     MembersCommand, GroupsCommand, SetMembersCommand,
     VerifyPlaintextPasswordCommand, VerifyHTTPDigestCommand,
     WikiAccessForUIDCommand, ContinuationCommand,
-    ExternalDelegatesCommand, StatsCommand, ExpandedMemberUIDsCommand
-    # UpdateRecordsCommand, RemoveRecordsCommand
+    ExternalDelegatesCommand, StatsCommand, ExpandedMemberUIDsCommand,
+    AddMembersCommand, RemoveMembersCommand,
+    UpdateRecordsCommand, # RemoveRecordsCommand,
 )
 from txdav.who.cache import CachingDirectoryService
 from txdav.who.util import directoryFromConfig
@@ -396,6 +397,66 @@
         returnValue(response)
 
 
+    @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)
+
+
     @SetMembersCommand.responder
     @inlineCallbacks
     def setMembers(self, uid, memberUIDs):
@@ -426,6 +487,31 @@
         returnValue(response)
 
 
+    @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)
+
+
     @GroupsCommand.responder
     @inlineCallbacks
     def groups(self, uid):

Modified: CalendarServer/trunk/txdav/dps/test/test_client.py
===================================================================
--- 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)
@@ -81,8 +81,11 @@
 
         # The "remote" directory service
         if testMode == "xml":
-            path = os.path.join(os.path.dirname(__file__), "test.xml")
-            remoteDirectory = CalendarXMLDirectoryService(FilePath(path))
+            # 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)
         elif testMode == "od":
             remoteDirectory = CalendarODDirectoryService()
 
@@ -95,16 +98,16 @@
         # actual networking
         self.patch(self.directory, "_getConnection", lambda: succeed(client))
 
-        # Wrap the normal _call method with one that flushes the IOPump
+        # Wrap the normal _sendCommand method with one that flushes the IOPump
         # afterwards
-        origCall = self.directory._call
+        origCall = self.directory._sendCommand
 
         def newCall(*args, **kwds):
             d = origCall(*args, **kwds)
             pump.flush()
             return d
 
-        self.patch(self.directory, "_call", newCall)
+        self.patch(self.directory, "_sendCommand", newCall)
 
 
     @inlineCallbacks
@@ -286,6 +289,53 @@
 
 
     @inlineCallbacks
+    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
     def test_verifyPlaintextPassword(self):
         expectations = (
             (testPassword, True),  # Correct
@@ -369,16 +419,16 @@
         # actual networking
         self.patch(self.client, "_getConnection", lambda: succeed(client))
 
-        # Wrap the normal _call method with one that flushes the IOPump
+        # Wrap the normal _sendCommand method with one that flushes the IOPump
         # afterwards
-        origCall = self.client._call
+        origCall = self.client._sendCommand
 
         def newCall(*args, **kwds):
             d = origCall(*args, **kwds)
             pump.flush()
             return d
 
-        self.patch(self.client, "_call", newCall)
+        self.patch(self.client, "_sendCommand", newCall)
 
 
     def configure(self):
@@ -620,6 +670,53 @@
 
 
     @inlineCallbacks
+    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
     def test_verifyPlaintextPassword(self):
         expectations = (
             (u"zehcnasw", True),  # Correct

Modified: CalendarServer/trunk/txdav/who/augment.py
===================================================================
--- 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)
@@ -294,7 +294,8 @@
             # Split out the base fields from the augment fields
             baseFields, augmentFields = self._splitFields(record)
 
-            if augmentFields:
+            # Ignore groups for now
+            if augmentFields and record.recordType != RecordType.group:
                 # Create an AugmentRecord
                 autoScheduleMode = {
                     AutoScheduleMode.none: "none",
@@ -324,7 +325,7 @@
                 augmentRecords.append(augmentRecord)
 
             # Create new base records:
-            baseRecords.append(DirectoryRecord(self._directory, baseFields))
+            baseRecords.append(DirectoryRecord(self._directory, record._baseRecord.fields if hasattr(record, "_baseRecord") else baseFields))
 
         # Apply the augment records
         if augmentRecords:
@@ -494,6 +495,18 @@
         returnValue(augmented)
 
 
+    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)
+
+
     @timed
     @inlineCallbacks
     def groups(self):

Modified: CalendarServer/trunk/txdav/who/xml.py
===================================================================
--- 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)
@@ -186,3 +186,13 @@
     recordTypeValue = ConstantsContainer(
         (BaseDirectoryService.recordTypeValue, RecordTypeValue)
     )
+
+
+    xmlFieldOrder = BaseDirectoryService.xmlFieldOrder + (
+        FieldName.capacity,
+        FieldName.floor,
+        FieldName.associatedAddress,
+        FieldName.abbreviatedName,
+        FieldName.streetAddress,
+        FieldName.geographicLocation,
+    )
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140922/69120a3a/attachment-0001.html>


More information about the calendarserver-changes mailing list