Monotone-Parent: 14fb6c252ef1563d917c59f289ac3d06479a7eab

Monotone-Revision: d085b8d79d3eca94ece4f311bf3d652d7f20bb00

Monotone-Author: wsourdeau@inverse.ca
Monotone-Date: 2009-08-10T20:59:49
Monotone-Branch: ca.inverse.sogo
maint-2.0.2
Wolfgang Sourdeau 2009-08-10 20:59:49 +00:00
parent 89b67f8434
commit ecfd8f026e
4 changed files with 204 additions and 39 deletions

View File

@ -6,6 +6,13 @@
2009-08-10 Wolfgang Sourdeau <wsourdeau@inverse.ca>
* 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.

View File

@ -32,6 +32,7 @@
#import <NGCards/iCalEvent.h>
#import <NGCards/iCalEventChanges.h>
#import <NGCards/iCalPerson.h>
#import <NGCards/NSCalendarDate+NGCards.h>
#import <SaxObjC/XMLNamespaces.h>
#import <SoObjects/SOGo/iCalEntityObject+Utilities.h>
@ -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];
}

View File

@ -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" );
};

View File

@ -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,