From ecfd8f026e2bfcaf1c5c4612a362d1e97dd6a306 Mon Sep 17 00:00:00 2001 From: Wolfgang Sourdeau Date: Mon, 10 Aug 2009 20:59:49 +0000 Subject: [PATCH] Monotone-Parent: 14fb6c252ef1563d917c59f289ac3d06479a7eab Monotone-Revision: d085b8d79d3eca94ece4f311bf3d652d7f20bb00 Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2009-08-10T20:59:49 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 7 + .../Appointments/SOGoAppointmentObject.m | 122 ++++++++++++++---- SoObjects/Appointments/product.plist | 4 +- Tests/test-davacl.py | 110 +++++++++++++--- 4 files changed, 204 insertions(+), 39 deletions(-) diff --git a/ChangeLog b/ChangeLog index 0cdf1a778..fbb7b7bf0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -6,6 +6,13 @@ 2009-08-10 Wolfgang Sourdeau + * SoObjects/Appointments/SOGoAppointmentObject.m (-PUTAction): + modified to handle the case where the active user has the "respond + to" permission, in which case we grab the copy of the iCalendar + which is already in the DB and modify the partstats thereof based + on the entries found in the uploaded iCalendar. The latter is + discarded silently to avoid any further change. + * SoObjects/Appointments/iCalEntityObject+SOGo.m (-userAsParticipant): new method that returns the "PARTICIPANT" entity of the SOGoUser passed as parameter, if found. diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index c9cba927e..0f6ac3bfa 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -32,6 +32,7 @@ #import #import #import +#import #import #import @@ -1245,52 +1246,129 @@ return @"IPM.Appointment"; } -// -// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we -// simply invoke super's PUTAction. -// -- (id) PUTAction: (WOContext *) _ctx +- (NSDictionary *) _partStatsFromCalendar: (iCalCalendar *) calendar +{ + NSMutableDictionary *partStats; + NSArray *allEvents; + int count, max; + iCalEvent *currentEvent; + iCalPerson *ownerParticipant; + NSString *key; + SOGoUser *ownerUser; + + ownerUser = [SOGoUser userWithLogin: owner roles: nil]; + + allEvents = [calendar events]; + max = [allEvents count]; + partStats = [NSMutableDictionary dictionaryWithCapacity: max]; + + for (count = 0; count < max; count++) + { + currentEvent = [allEvents objectAtIndex: count]; + ownerParticipant = [currentEvent userAsParticipant: ownerUser]; + if (ownerParticipant) + { + if (count == 0) + key = @"master"; + else + key = [[currentEvent recurrenceId] iCalFormattedDateTimeString]; + [partStats setObject: ownerParticipant forKey: key]; + } + } + + return partStats; +} + +- (void) _setupResponseCalendarInRequest: (WORequest *) rq +{ + iCalCalendar *calendar, *putCalendar; + NSData *newContent; + NSArray *keys; + NSDictionary *partStats, *newPartStats; + NSString *partStat, *key; + int count, max; + + calendar = [self calendar: NO secure: NO]; + partStats = [self _partStatsFromCalendar: calendar]; + keys = [partStats allKeys]; + max = [keys count]; + if (max > 0) + { + putCalendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; + newPartStats = [self _partStatsFromCalendar: putCalendar]; + if ([keys isEqualToArray: [newPartStats allKeys]]) + { + for (count = 0; count < max; count++) + { + key = [keys objectAtIndex: count]; + partStat = [[newPartStats objectForKey: key] partStat]; + [[partStats objectForKey: key] setPartStat: partStat]; + } + } + } + + newContent = [[calendar versitString] + dataUsingEncoding: [rq contentEncoding]]; + [rq setContent: newContent]; +} + +- (void) _decomposeGroupsInRequest: (WORequest *) rq { iCalCalendar *calendar; NSArray *allEvents; iCalEvent *event; - WORequest *rq; - - BOOL b; int i; + BOOL modified; - rq = [_ctx request]; + // If we decomposed at least one group, let's rewrite the content + // of the request. Otherwise, leave it as is in case this rewrite + // isn't totaly lossless. + calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; - if ([[rq headersForKey: @"X-SOGo"] containsObject: @"NoGroupsDecomposition"]) - return [super PUTAction: _ctx]; - - //NSLog(@"Content from request: %@", [rq contentAsString]); - - // The algorithm is pretty straightforward: + // The algorithm is pretty straightforward: // // We get all events // We get all attendees // If some are groups, we decompose them // We regenerate the iCalendar string // - calendar = [iCalCalendar parseSingleFromSource: [rq contentAsString]]; allEvents = [calendar events]; - b = NO; + modified = NO; for (i = 0; i < [allEvents count]; i++) { event = [allEvents objectAtIndex: i]; - if ([self expandGroupsInEvent: event]) - b = YES; + modified |= [self expandGroupsInEvent: event]; } - //NSLog(@"Content from calendar:secure: %@", [calendar versitString]); - // If we decomposed at least one group, let's rewrite the content // of the request. Otherwise, leave it as is in case this rewrite // isn't totaly lossless. - if (b) + if (modified) [rq setContent: [[calendar versitString] dataUsingEncoding: [rq contentEncoding]]]; +} + +// +// If we see "X-SOGo: NoGroupsDecomposition" in the HTTP headers, we +// simply invoke super's PUTAction. +// +- (id) PUTAction: (WOContext *) _ctx +{ + WORequest *rq; + NSArray *roles; + + rq = [_ctx request]; + + roles = [[context activeUser] rolesForObject: self inContext: context]; + if ([roles containsObject: @"ComponentResponder"] + && ![roles containsObject: @"ComponentModifier"]) + [self _setupResponseCalendarInRequest: rq]; + else + { + if (![[rq headersForKey: @"X-SOGo"] + containsObject: @"NoGroupsDecomposition"]) + [self _decomposeGroupsInRequest: rq]; + } return [super PUTAction: _ctx]; } diff --git a/SoObjects/Appointments/product.plist b/SoObjects/Appointments/product.plist index 619b5be7c..96660024f 100644 --- a/SoObjects/Appointments/product.plist +++ b/SoObjects/Appointments/product.plist @@ -41,7 +41,7 @@ "ModifyComponent" = ( "Owner", "ComponentModifier", "ObjectEditor" ); "RespondToComponent" = ( "Owner", "ComponentModifier", "ComponentResponder" ); "Access Object" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" ); - "Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ObjectEditor" ); + "Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ComponentResponder", "ObjectEditor" ); "Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" ); "WebDAV Access" = ( "Owner", "Organizer", "ObjectCreator", "ObjectEraser", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" ); }; @@ -61,7 +61,7 @@ "ModifyComponent" = ( "Owner", "ComponentModifier" ); "RespondToComponent" = ( "Participant", "ComponentModifier", "ComponentResponder" ); "Access Object" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" ); - "Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier" ); + "Change Images and Files" = ( "Owner", "Organizer", "ComponentModifier", "ComponentResponder" ); "Access Contents Information" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" ); "WebDAV Access" = ( "Owner", "Organizer", "Participant", "ComponentModifier", "ComponentResponder", "ComponentViewer", "ComponentDAndTViewer" ); }; diff --git a/Tests/test-davacl.py b/Tests/test-davacl.py index e3b5ce74b..d2068816a 100755 --- a/Tests/test-davacl.py +++ b/Tests/test-davacl.py @@ -9,14 +9,31 @@ import xml.xpath import time # TODO: -# - cal: we don't test "respond" yet +# - cal: complete test for "modify": "respond to" causes a 204 but no actual +# modification should occur # - ab: testcase for addressbook-query, webdav-sync (no "calendar-data" # equivalent) -# ? cal: testcase for "calendar-query" +# - cal: testcase for "calendar-query" # - test rights validity: # - send invalid rights to SOGo and expect failures # - refetch the set of rights and make sure it matches what was set # originally +# - test "current-user-acl-set" + +def fetchUserEmail(login): + client = webdavlib.WebDAVClient(hostname, port, + username, password) + resource = '/SOGo/dav/%s/' % login + propfind = webdavlib.WebDAVPROPFIND(resource, + ["{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], + 0) + client.execute(propfind) + xpath_context = xml.xpath.CreateContext(propfind.response["document"]) + xpath_context.setNamespaces({ "D": "DAV:", + "C": "urn:ietf:params:xml:ns:caldav" }) + nodes = xml.xpath.Evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', None, xpath_context) + + return nodes[0].childNodes[0].nodeValue class DAVAclTest(unittest.TestCase): resource = None @@ -102,15 +119,18 @@ DTEND:20090805T140000Z CLASS:%(class)s DESCRIPTION:%(class)s description LOCATION:location -CREATED:20090805T100000Z +%(organizer_line)s%(attendee_line)sCREATED:20090805T100000Z DTSTAMP:20090805T100000Z END:VEVENT END:VCALENDAR""" class DAVCalendarAclTest(DAVAclTest): resource = '/SOGo/dav/%s/Calendar/test-dav-acl/' % username + user_email = None def setUp(self): + if self.user_email is None: + self.user_email = fetchUserEmail(username) DAVAclTest.setUp(self) self.classToICSClass = { "pu": "PUBLIC", "pr": "PRIVATE", @@ -139,15 +159,32 @@ class DAVCalendarAclTest(DAVAclTest): """'create', 'delete', 'view d&t' PUBLIC, 'modify' PRIVATE""" self._testRights({ "c": True, "d": True, "pu": "d", "pr": "m" }) + def testCreateRespondToPublic(self): + """'create', 'respond to' PUBLIC""" + self._testRights({ "c": True, "pu": "r" }) + def testNothing(self): """no right given""" self._testRights({}) def _putEvent(self, client, filename, - event_class = "PUBLIC", exp_status = 201): + event_class = "PUBLIC", + exp_status = 201, + organizer = None, attendee = None, + partstat = "NEEDS-ACTION"): url = "%s%s" % (self.resource, filename) + if organizer is not None: + organizer_line = "ORGANIZER:%s\n" % organizer + else: + organizer_line = "" + if attendee is not None: + attendee_line = "ATTENDEE;PARTSTAT=%s:%s\n" % (partstat, attendee) + else: + attendee_line = "" event = event_template % { "class": event_class, - "filename": filename } + "filename": filename, + "organizer_line": organizer_line, + "attendee_line": attendee_line } put = webdavlib.HTTPPUT(url, event, "text/calendar; charset=utf-8") client.execute(put) self.assertEquals(put.response["status"], exp_status, @@ -219,19 +256,24 @@ class DAVCalendarAclTest(DAVAclTest): right = None event = self._getEvent(event_class) - self._checkEventRight("GET", event, event_class, right) + self._checkViewEventRight("GET", event, event_class, right) event = self._propfindEvent(event_class) - self._checkEventRight("PROPFIND", event, event_class, right) + self._checkViewEventRight("PROPFIND", event, event_class, right) event = self._multigetEvent(event_class) - self._checkEventRight("multiget", event, event_class, right) + self._checkViewEventRight("multiget", event, event_class, right) event = self._webdavSyncEvent(event_class) - self._checkEventRight("webdav-sync", event, event_class, right) + self._checkViewEventRight("webdav-sync", event, event_class, right) self._testModify(event_class, right) + self._testRespondTo(event_class, right) - def _getEvent(self, event_class): + def _getEvent(self, event_class, is_invitation = False): icsClass = self.classToICSClass[event_class] - url = "%s%s.ics" % (self.resource, icsClass.lower()) + if is_invitation: + filename = "invitation-%s" % icsClass.lower() + else: + filename = "%s" % icsClass.lower() + url = "%s%s.ics" % (self.resource, filename) get = webdavlib.HTTPGET(url) self.subscriber_client.execute(get) @@ -315,7 +357,7 @@ class DAVCalendarAclTest(DAVAclTest): return event - def _checkEventRight(self, operation, event, event_class, right): + def _checkViewEventRight(self, operation, event, event_class, right): if right is None: self.assertEquals(event, None, "None right expecting event invisibility for" @@ -327,8 +369,10 @@ class DAVCalendarAclTest(DAVAclTest): if right == "v" or right == "r" or right == "m": icsClass = self.classToICSClass[event_class] complete_event = (event_template % { "class": icsClass, - "filename": "%s.ics" % icsClass.lower() }) - self.assertTrue(event == complete_event, + "filename": "%s.ics" % icsClass.lower(), + "organizer_line": "", + "attendee_line": ""}) + self.assertTrue(event.strip() == complete_event.strip(), "Right '%s' should return complete event" " during operation '%s'" % (right, operation)) @@ -367,7 +411,7 @@ class DAVCalendarAclTest(DAVAclTest): % key) def _testModify(self, event_class, right): - if right == "m": + if right == "m" or right == "r": exp_code = 204 else: exp_code = 403 @@ -376,6 +420,42 @@ class DAVCalendarAclTest(DAVAclTest): self._putEvent(self.subscriber_client, filename, icsClass, exp_code) + def _testRespondTo(self, event_class, right): + icsClass = self.classToICSClass[event_class] + filename = "invitation-%s.ics" % icsClass.lower() + self._putEvent(self.client, filename, icsClass, + 201, + "mailto:nobody@somewhere.com", self.user_email, + "NEEDS-ACTION") + + if right == "m" or right == "r": + exp_code = 204 + else: + exp_code = 403 + + # here we only do 'passive' validation: if a user has a "respond to" + # right, only the attendee entry will me modified. The change of + # organizer must thus be silently ignored below. + self._putEvent(self.subscriber_client, filename, icsClass, + exp_code, "mailto:someone@nowhere.com", self.user_email, + "ACCEPTED") + if exp_code == 204: + att_line = "ATTENDEE;PARTSTAT=ACCEPTED:%s\n" % self.user_email + if right == "r": + exp_event = event_template % {"class": icsClass, + "filename": filename, + "organizer_line": "ORGANIZER:mailto:nobody@somewhere.com\n", + "attendee_line": att_line} + else: + exp_event = event_template % {"class": icsClass, + "filename": filename, + "organizer_line": "ORGANIZER:mailto:someone@nowhere.com\n", + "attendee_line": att_line} + event = self._getEvent(event_class, True).replace("\r", "") + self.assertEquals(exp_event.strip(), event.strip(), + "'respond to' event does not match:\nreceived:\n" + "%s\nexpected:\n%s" % (event, exp_event)) + # Addressbook: # short rights notation: { "c": create, # "d": delete,