[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