[CalendarServer-changes] [14022] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 30 01:39:00 PDT 2014


Revision: 14022
          http://trac.calendarserver.org//changeset/14022
Author:   cdaboo at apple.com
Date:     2014-09-30 01:39:00 -0700 (Tue, 30 Sep 2014)
Log Message:
-----------
Several fixes for group sharing changes. More work still to do.

Modified Paths:
--------------
    CalendarServer/trunk/conf/auth/augments-test.xml
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/requirements-dev.txt
    CalendarServer/trunk/requirements-stable.txt
    CalendarServer/trunk/twistedcaldav/sharing.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/who/test/test_group_sharees.py

Modified: CalendarServer/trunk/conf/auth/augments-test.xml
===================================================================
--- CalendarServer/trunk/conf/auth/augments-test.xml	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/conf/auth/augments-test.xml	2014-09-30 08:39:00 UTC (rev 14022)
@@ -1,23 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<!--
-Copyright (c) 2006-2014 Apple Inc. All rights reserved.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
- -->
-
-<!DOCTYPE augments SYSTEM "augments.dtd">
-
 <augments>
 <record>
     <uid>Default</uid>
@@ -79,4 +59,4 @@
     <auto-schedule-mode>decline-always</auto-schedule-mode>
     <auto-accept-group>20000000-0000-0000-0000-000000000001</auto-accept-group>
 </record>
-</augments>
+</augments>
\ No newline at end of file

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2014-09-30 08:39:00 UTC (rev 14022)
@@ -793,6 +793,10 @@
     	<dict>
 	    	<key>Enabled</key>
 	    	<true/>
+
+			<!-- Make these short for testing -->
+	    	<key>ReconciliationDelaySeconds</key>
+	    	<real>0.1</real>
     	</dict>
       </dict>
       <key>AddressBooks</key>

Modified: CalendarServer/trunk/requirements-dev.txt
===================================================================
--- CalendarServer/trunk/requirements-dev.txt	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/requirements-dev.txt	2014-09-30 08:39:00 UTC (rev 14022)
@@ -7,4 +7,4 @@
 mockldap
 q
 --editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVClientLibrary/trunk@13420#egg=CalDAVClientLibrary
---editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@13957#egg=CalDAVTester
+--editable svn+http://svn.calendarserver.org/repository/calendarserver/CalDAVTester/trunk@14021#egg=CalDAVTester

Modified: CalendarServer/trunk/requirements-stable.txt
===================================================================
--- CalendarServer/trunk/requirements-stable.txt	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/requirements-stable.txt	2014-09-30 08:39:00 UTC (rev 14022)
@@ -5,7 +5,7 @@
 # For CalendarServer development, don't try to get these projects from PyPI; use svn.
 
 -e .
--e svn+http://svn.calendarserver.org/repository/calendarserver/twext/trunk@13987#egg=twextpy
+-e svn+http://svn.calendarserver.org/repository/calendarserver/twext/trunk@14020#egg=twextpy
 -e svn+http://svn.calendarserver.org/repository/calendarserver/PyKerberos/trunk@13420#egg=kerberos
 -e svn+http://svn.calendarserver.org/repository/calendarserver/PyCalendar/trunk@13802#egg=pycalendar
 

Modified: CalendarServer/trunk/twistedcaldav/sharing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/twistedcaldav/sharing.py	2014-09-30 08:39:00 UTC (rev 14022)
@@ -416,8 +416,10 @@
             userid is not allowed.
         """
 
-        # First try to resolve as a principal
+        # First try to resolve as a calendar principal
         principal = yield self.principalForCalendarUserAddress(userid)
+        if principal is None:
+            principal = yield self.principalForCalendarGroupAddress(userid)
         if principal:
             if request:
                 ownerPrincipal = (yield self.ownerPrincipal(request))

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2014-09-30 08:39:00 UTC (rev 14022)
@@ -81,7 +81,7 @@
     AttachmentSizeTooLarge, UnknownTimezone
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
-    CommonObjectResource, ECALENDARTYPE
+    CommonObjectResource, ECALENDARTYPE, SharingInvitation
 from txdav.common.datastore.sql_tables import _ATTACHMENTS_MODE_NONE, \
     _ATTACHMENTS_MODE_READ, _ATTACHMENTS_MODE_WRITE, _BIND_MODE_DIRECT, \
     _BIND_MODE_GROUP, _BIND_MODE_GROUP_READ, _BIND_MODE_GROUP_WRITE, \
@@ -1752,17 +1752,47 @@
             resourceID=child._resourceID
         )
 
+
     # ===============================================================================
     # Group sharing
     # ===============================================================================
 
+    # We need to handle the case where a sharee is added directly or as a member of one
+    # or more groups. In order to do that we distinguish those cases via the BIND_MODE:
+    #
+    # BIND_MODE_READ, BIND_MODE_WRITE - an individual sharee only (i.e., not a member of
+    #    a group also being shared to).
+    #
+    # BIND_MODE_GROUP - a sharee added solely as a member of one or more groups (i.e., not
+    #    an individual).
+    #
+    # BIND_MODE_GROUP_READ, BIND_MODE_GROUP_WRITE - a sharee added as both an individual
+    #    and as a member of a group). The READ/WRITE state reflects the individual
+    #    access granted).
+    #
+    # For the BIND_MODE_GROUP* variants the actual access level is determined by the
+    #    max of all the group access levels and the individual access level. That
+    #    that is determined by the L{effectiveShareMode} method.
+    #
+    # When adding/removing an individual or reconciling group membership changes, the
+    #    actual BIND_MODE in the bind table is determined by one of the _groupModeAfter*
+    #    methods.
 
     @inlineCallbacks
     def reconcileGroupSharee(self, groupUID):
         """
-        reconcile bind table with group members
+        Reconcile bind table with group members. Note that when this is called, changes have
+        already been made to the group being refreshed. So members that have been removed
+        won't appear as a member of the group but will still have an associated bind row.
+
+        What we need to do is get the list of all existing group binds and diff that with the
+        list of expected group binds from all groups shared to this calendar. This means we will
+        end up also reconciling other shared groups that changed. There is no way to prevent that
+        short of recording in the bind table, which groups caused a member to be bound.
         """
         changed = False
+
+        # First check that the actual group membership has changed
         if (yield self.updateShareeGroupLink(groupUID)):
             groupID = (yield self._txn.groupByUID(groupUID))[0]
             memberUIDs = yield self._txn.groupMemberUIDs(groupID)
@@ -1874,7 +1904,7 @@
         rows = yield Select(
             [Count(gs.GROUP_ID)],
             From=gs,
-            Where=(
+            Where=(gs.CALENDAR_ID == self._resourceID).And(
                 gs.GROUP_ID.In(
                     Select(
                         [gm.GROUP_ID],
@@ -1886,7 +1916,7 @@
                 )
             )
         ).on(self._txn, uid=self.viewerHome().uid())
-        if rows[0][0] > 1:
+        if rows[0][0] > 0:
             # no mode change for group shares
             result = {
                 _BIND_MODE_GROUP: _BIND_MODE_GROUP,
@@ -1949,9 +1979,8 @@
         returnValue(changed)
 
 
-    @classmethod
     @inlineCallbacks
-    def _effectiveShareMode(cls, bindMode, viewerUID, txn):
+    def _effectiveShareMode(self, bindMode, viewerUID, txn):
         """
         Get the effective share mode without a calendar object
         """
@@ -1963,7 +1992,7 @@
             rows = yield Select(
                 [Max(gs.GROUP_BIND_MODE)], # _BIND_MODE_WRITE > _BIND_MODE_READ
                 From=gs,
-                Where=(
+                Where=(gs.CALENDAR_ID == self._resourceID).And(
                     gs.GROUP_ID.In(
                         Select(
                             [gm.GROUP_ID],
@@ -1975,7 +2004,11 @@
                     )
                 )
             ).on(txn, uid=viewerUID)
-            returnValue(rows[0][0])
+
+            # The result might be empty if the sharee has been removed from the group, but reconciliation has not
+            # yet occurred, so cope with that by giving them the lowest mode since we don't know what the last valid
+            # mode was.
+            returnValue(rows[0][0] if rows[0][0] else _BIND_MODE_READ)
         else:
             returnValue(bindMode)
 
@@ -2004,6 +2037,7 @@
         @type summary: C{str}
         """
 
+        # Go direct to individual sharing API if groups are disabled
         if not (
                 config.Sharing.Enabled and
                 config.Sharing.Calendars.Enabled and
@@ -2072,22 +2106,29 @@
         @type shareeUID: C{str}
         """
 
-        rows = None
-        groupID = (yield self._txn.groupByUID(shareeUID, create=False))[0]
-        if groupID is not None:
-            gs = schema.GROUP_SHAREE
-            rows = yield Select(
-                [gs.GROUP_ID],
-                From=gs,
-                Where=(gs.CALENDAR_ID == self._resourceID).And(
-                    gs.GROUP_ID == groupID
-                ),
-            ).on(self._txn)
+        # Go direct to individual sharing API if groups are disabled
+        if not (
+                config.Sharing.Enabled and
+                config.Sharing.Calendars.Enabled and
+                config.Sharing.Calendars.Groups.Enabled
+        ):
+            returnValue((yield super(Calendar, self).uninviteUIDFromShare(shareeUID)))
 
+        # Check if the sharee is a group
+        gr = schema.GROUPS
+        gs = schema.GROUP_SHAREE
+        rows = yield Select(
+            [gs.GROUP_ID],
+            From=gs.join(gr, gs.GROUP_ID == gr.GROUP_ID),
+            Where=gr.GROUP_UID == shareeUID
+        ).on(self._txn)
+
         if rows:
+            groupID = rows[0][0]
             reinvites = []
-            # uninvite each member of group
-            memberUIDs = yield self._txn.groupMemberUIDs(rows[0][0])
+
+            # Uninvite each member of group
+            memberUIDs = yield self._txn.groupMemberUIDs(groupID)
             for memberUID in memberUIDs:
                 if memberUID != self._home.uid():
                     shareeView = yield self.shareeView(memberUID)
@@ -2125,6 +2166,40 @@
                     yield super(Calendar, self).inviteUIDToShare(shareeUID, newMode)
 
 
+    @inlineCallbacks
+    def allInvitations(self):
+        """
+        Get list of all invitations (non-direct) to this object.
+        """
+        user_invitations = yield super(Calendar, self).allInvitations()
+
+        # Add groups (before the individuals)
+        gs = schema.GROUP_SHAREE
+        gr = schema.GROUPS
+        rows = yield Select(
+            [gr.GROUP_UID, gs.GROUP_BIND_MODE],
+            From=gs.join(gr, gs.GROUP_ID == gr.GROUP_ID),
+            Where=(gs.CALENDAR_ID == self._resourceID),
+        ).on(self._txn)
+
+        invitations = []
+        for guid, gmode in rows:
+            invite = SharingInvitation(
+                guid,
+                self.ownerHome().name(),
+                self.ownerHome().id(),
+                guid,
+                0,
+                gmode,
+                _BIND_STATUS_ACCEPTED,
+                "",
+            )
+            invitations.append(invite)
+
+        invitations.extend(user_invitations)
+        returnValue(invitations)
+
+
 icalfbtype_to_indexfbtype = {
     "UNKNOWN"         : 0,
     "FREE"            : 1,

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2014-09-30 08:39:00 UTC (rev 14022)
@@ -5294,8 +5294,7 @@
         return self._bindMode
 
 
-    @classmethod
-    def _effectiveShareMode(cls, bindMode, viewerUID, txn):
+    def _effectiveShareMode(self, bindMode, viewerUID, txn):
         """
         Get the effective share mode without a calendar object
         """

Modified: CalendarServer/trunk/txdav/who/test/test_group_sharees.py
===================================================================
--- CalendarServer/trunk/txdav/who/test/test_group_sharees.py	2014-09-30 08:05:21 UTC (rev 14021)
+++ CalendarServer/trunk/txdav/who/test/test_group_sharees.py	2014-09-30 08:39:00 UTC (rev 14022)
@@ -21,12 +21,12 @@
 from twext.enterprise.jobqueue import JobItem
 from twext.python.filepath import CachingFilePath as FilePath
 from twisted.internet import reactor
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twisted.trial import unittest
 from twistedcaldav.config import config
 from txdav.caldav.datastore.test.util import populateCalendarsFrom, CommonCommonTests
 from txdav.who.directory import CalendarDirectoryRecordMixin
-from txdav.who.groups import GroupCacher
+from txdav.who.groups import GroupCacher, GroupShareeReconciliationWork
 import os
 from txdav.common.datastore.sql_tables import _BIND_MODE_GROUP
 from txdav.common.datastore.sql_tables import _BIND_MODE_READ
@@ -262,3 +262,89 @@
         yield calendar.uninviteUIDFromShare("group04")
         noinvites = yield calendar.sharingInvites()
         self.assertEqual(len(noinvites), 0)
+
+
+    @inlineCallbacks
+    def test_group_member_removal_refresh_slow(self):
+        """
+        Test that the sharee list is still valid when a member is removed from a group, but
+        sharee reconciliation has not yet occurred.
+        """
+
+        @inlineCallbacks
+        def expandedMembers(self, records=None, seen=None):
+
+            if self.uid == "group02":
+                returnValue(frozenset())
+            else:
+                returnValue((yield unpatchedExpandedMembers(self, records, seen)))
+
+        unpatchedExpandedMembers = CalendarDirectoryRecordMixin.expandedMembers
+
+        # Prevent sharee reconciliation
+        def _noop(self):
+            return succeed(None)
+        self.patch(GroupShareeReconciliationWork, "doWork", _noop)
+
+        # setup group cacher
+        groupCacher = GroupCacher(self.transactionUnderTest().directoryService())
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 0)
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group02")
+        self.assertEqual(len(wps), 0)
+
+        yield self._check_notifications("user01", [])
+
+        # Invite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 0)
+        self.assertFalse(calendar.isShared())
+
+        yield self._check_notifications("user01", [])
+        shareeViews = yield calendar.inviteUIDToShare("group02", _BIND_MODE_READ)
+        self.assertEqual(len(shareeViews), 3)
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 3)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        groupsToRefresh = yield groupCacher.groupsToRefresh(self.transactionUnderTest())
+        self.assertEqual(len(groupsToRefresh), 1)
+
+        # 0 group members
+        self.patch(CalendarDirectoryRecordMixin, "expandedMembers", expandedMembers)
+
+        wps = yield groupCacher.refreshGroup(self.transactionUnderTest(), "group02")
+        self.assertEqual(len(wps), 1)
+        yield self.commit()
+        yield JobItem.waitEmpty(self._sqlCalendarStore.newTransaction, reactor, 60)
+
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        invites = yield calendar.sharingInvites()
+        self.assertEqual(len(invites), 3)
+        for invite in invites:
+            shareeView = yield calendar.shareeView(invite.shareeUID)
+            self.assertEqual(invite.ownerUID, "user01")
+            self.assertEqual(invite.uid, shareeView.shareName())
+            self.assertEqual(invite.mode, _BIND_MODE_GROUP)
+            self.assertEqual((yield shareeView.effectiveShareMode()), _BIND_MODE_READ)
+            self.assertEqual(invite.status, _BIND_STATUS_INVITED)
+            self.assertEqual(invite.summary, None)
+            yield self._check_notifications(invite.shareeUID, [invite.uid, ])
+
+        yield self._check_notifications("user01", [])
+
+        # Uninvite
+        calendar = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar.uninviteUIDFromShare("group02")
+        noinvites = yield calendar.sharingInvites()
+        self.assertEqual(len(noinvites), 3)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140930/20445846/attachment-0001.html>


More information about the calendarserver-changes mailing list