diff --git a/ChangeLog b/ChangeLog index 90d56719c..b16794b59 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,21 @@ +2010-07-14 Wolfgang Sourdeau + + * SoObjects/SOGo/SOGoGCSFolder.m (-aclSQLListingFilter): return an + empty string when the active user is a super user. + (-initializeQuickTablesAclsInContext:): set + "userCanAccessAllObjects" to YES also when the active user is a + super user. + + * Tests/Integration/test-davacl.py + (DAVCalendarAclTest._testEventIsSecureVersion): accept a differing + SUMMARY since it will always change depending on the user's + language and is a pain to adapt. + (DAVCalendarPublicAclTest.testCollectionAccessNormalUser): added + code to accept and handle the XML and ICS calendar variants. + (DAVCalendarSuperUserAclTest.__init__): new test access class for + ensuring that super users have full, inconditionnal and complete + access to simple user's collections + 2010-07-13 Wolfgang Sourdeau * Tests/Integration/test-put.py: new test module executing X puts diff --git a/Scripts/sql-update-1.2.2_to_1.2.3.sh b/Scripts/sql-update-1.2.2_to_1.3.0.sh similarity index 100% rename from Scripts/sql-update-1.2.2_to_1.2.3.sh rename to Scripts/sql-update-1.2.2_to_1.3.0.sh diff --git a/SoObjects/SOGo/SOGoGCSFolder.m b/SoObjects/SOGo/SOGoGCSFolder.m index 12f512f77..74639d389 100644 --- a/SoObjects/SOGo/SOGoGCSFolder.m +++ b/SoObjects/SOGo/SOGoGCSFolder.m @@ -594,14 +594,20 @@ static NSArray *childRecordFields = nil; [self _subscriberRenameTo: newName]; } +/* Returns an empty string to indicate that the filter is empty and nil when + the query should not even be performed. */ - (NSString *) aclSQLListingFilter { NSString *filter, *login; NSArray *roles; + SOGoUser *activeUser; - login = [[context activeUser] login]; + activeUser = [context activeUser]; + login = [activeUser login]; if (activeUserIsOwner - || [[self ownerInContext: nil] isEqualToString: login]) + || [[self ownerInContext: nil] isEqualToString: login] + || ([activeUser respondsToSelector: @selector (isSuperUser)] + && [activeUser isSuperUser])) filter = @""; else { @@ -613,9 +619,6 @@ static NSArray *childRecordFields = nil; filter = nil; } - /* An empty string indicates that the filter is empty while a return value - of nil indicates that the query should not even be performed. */ - return filter; } @@ -1349,17 +1352,21 @@ static NSArray *childRecordFields = nil; - (void) initializeQuickTablesAclsInContext: (WOContext *) localContext { NSString *login; + SOGoUser *activeUser; + activeUser = [localContext activeUser]; if (activeUserIsOwner) userCanAccessAllObjects = activeUserIsOwner; else { - login = [[localContext activeUser] login]; + login = [activeUser login]; /* we only grant "userCanAccessAllObjects" for role "ObjectEraser" and not "ObjectCreator" because the latter doesn't imply we can read properties from subobjects or even know their existence. */ userCanAccessAllObjects - = [[self ownerInContext: localContext] isEqualToString: login]; + = ([[self ownerInContext: localContext] isEqualToString: login] + || ([activeUser respondsToSelector: @selector (isSuperUser)] + && [activeUser isSuperUser])); } } diff --git a/Tests/Integration/test-davacl.py b/Tests/Integration/test-davacl.py index 4202e59bb..5455929d6 100755 --- a/Tests/Integration/test-davacl.py +++ b/Tests/Integration/test-davacl.py @@ -22,6 +22,149 @@ import utilities # originally # - test "current-user-acl-set" +class DAVCalendarSuperUserAclTest(unittest.TestCase): + def __init__(self, arg): + self.client = webdavlib.WebDAVClient(hostname, port, + username, password) + self.resource = "/SOGo/dav/%s/Calendar/test-dav-superuser-acl/" % subscriber_username + self.filename = "suevent.ics" + self.url = "%s%s" % (self.resource, self.filename) + + unittest.TestCase.__init__(self, arg) + + def setUp(self): + delete = webdavlib.WebDAVDELETE(self.resource) + self.client.execute(delete) + mkcol = webdavlib.WebDAVMKCOL(self.resource) + self.client.execute(mkcol) + self.assertEquals(mkcol.response["status"], 201, + "preparation: failure creating collection" + "(code = %d)" % mkcol.response["status"]) + + def tearDown(self): + delete = webdavlib.WebDAVDELETE(self.resource) + self.client.execute(delete) + + def _getEvent(self): + get = webdavlib.HTTPGET(self.url) + self.client.execute(get) + + if get.response["status"] == 200: + event = get.response["body"] + else: + event = None + + return event + + def _calendarDataInMultistatus(self, query, response_tag = "{DAV:}response"): + event = None + + # print "\n\n\n%s\n\n" % query.response["body"] + # print "\n\n" + response_nodes = query.response["document"].findall(response_tag) + for response_node in response_nodes: + href_node = response_node.find("{DAV:}href") + href = href_node.text + if href.endswith(self.filename): + propstat_node = response_node.find("{DAV:}propstat") + if propstat_node is not None: + status_node = propstat_node.find("{DAV:}status") + status = status_node.text + if status.endswith("200 OK"): + data_node = propstat_node.find("{DAV:}prop/{urn:ietf:params:xml:ns:caldav}calendar-data") + event = data_node.text + elif not (status.endswith("404 Resource Not Found") + or status.endswith("404 Not Found")): + self.fail("%s: unexpected status code: '%s'" + % (self.filename, status)) + + return event + + def _propfindEvent(self): + propfind = webdavlib.WebDAVPROPFIND(self.resource, + ["{urn:ietf:params:xml:ns:caldav}calendar-data"], + 1) + self.client.execute(propfind) + if propfind.response["status"] != 404: + event = self._calendarDataInMultistatus(propfind) + + return event + + def _multigetEvent(self): + event = None + + multiget = webdavlib.CalDAVCalendarMultiget(self.resource, + ["{urn:ietf:params:xml:ns:caldav}calendar-data"], + [ self.url ]) + self.client.execute(multiget) + if multiget.response["status"] != 404: + event = self._calendarDataInMultistatus(multiget) + + return event + + def _webdavSyncEvent(self): + event = None + + sync_query = webdavlib.WebDAVSyncQuery(self.resource, None, + ["{urn:ietf:params:xml:ns:caldav}calendar-data"]) + self.client.execute(sync_query) + if sync_query.response["status"] != 404: + event = self._calendarDataInMultistatus(sync_query, "{DAV:}sync-response") + + return event + + def testSUAccess(self): + """create, read, modify, delete for superuser""" + event = event_template % { "class": "PUBLIC", + "filename": self.filename, + "organizer_line": "", + "attendee_line": "" } + + # 1. Create + put = webdavlib.HTTPPUT(self.url, event) + put.content_type = "text/calendar; charset=utf-8" + self.client.execute(put) + self.assertEquals(put.response["status"], 201, + "%s: event creation/modification:" + " expected status code '201' (received '%d')" + % (self.filename, put.response["status"])) + + # 2. Read + readEvent = self._getEvent() + self.assertEquals(readEvent, event, + "GET: returned event does not match") + readEvent = self._propfindEvent() + self.assertEquals(readEvent, event, + "PROPFIND: returned event does not match") + readEvent = self._multigetEvent() + self.assertEquals(readEvent, event, + "MULTIGET: returned event does not match") + readEvent = self._webdavSyncEvent() + self.assertEquals(readEvent, event, + "WEBDAV-SYNC: returned event does not match") + + # 3. Modify + for eventClass in [ "CONFIDENTIAL", "PRIVATE", "PUBLIC" ]: + event = event_template % { "class": eventClass, + "filename": self.filename, + "organizer_line": "", + "attendee_line": "" } + put = webdavlib.HTTPPUT(self.url, event) + put.content_type = "text/calendar; charset=utf-8" + self.client.execute(put) + self.assertEquals(put.response["status"], 204, + "%s: event modification failed" + " expected status code '204' (received '%d')" + % (self.filename, put.response["status"])) + + # 4. Delete + delete = webdavlib.WebDAVDELETE(self.url) + self.client.execute(delete) + self.assertEquals(delete.response["status"], 204, + "%s: event deletion failed" + " expected status code '204' (received '%d')" + % (self.filename, put.response["status"])) + class DAVAclTest(unittest.TestCase): resource = None @@ -484,7 +627,8 @@ class DAVCalendarAclTest(DAVAclTest): for key in event_dict.keys(): self.assertTrue(expected_dict.has_key(key), "key '%s' of secure event not expected" % key) - self.assertTrue(expected_dict[key] == event_dict[key], + self.assertTrue(expected_dict[key] == event_dict[key] + or key == "SUMMARY", "value for key '%s' of secure does not match" " (exp: '%s', obtained: '%s'" % (key, expected_dict[key], event_dict[key] )) @@ -853,13 +997,22 @@ class DAVCalendarPublicAclTest(unittest.TestCase): acl_utility.setupRights(subscriber_username, { "c": True }) self.subscriber_client.execute(propfind) - hrefs = propfind.response["document"] \ - .findall("{DAV:}response/{DAV:}href") - self.assertEquals(len(hrefs), 2, "expected two hrefs in response") + hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}href") + self.assertEquals(len(hrefs), 4, + "expected 4 hrefs in response, got %d: %s" + % (len(hrefs), ", ".join([ x.text for x in hrefs ]))) self.assertEquals(hrefs[0].text, parentColl, "the first href is not a 'Calendar' parent coll.") - self.assertEquals(hrefs[1].text, resource, - "the 2nd href is not the accessible coll.") + + resourceHrefs = { resource: False, + "%s.xml" % resource[:-1]: False, + "%s.ics" % resource[:-1]: False } + for href in hrefs[1:]: + self.assertTrue(resourceHrefs.has_key(href.text), + "received unexpected href: %s" % href.text) + self.assertFalse(resourceHrefs[href.text], + "href was returned more than once: %s" % href.text) + resourceHrefs[href.text] = True acl_utility.setupRights(subscriber_username, {}) @@ -870,13 +1023,21 @@ class DAVCalendarPublicAclTest(unittest.TestCase): self.subscriber_client.execute(propfind) hrefs = propfind.response["document"] \ .findall("{DAV:}response/{DAV:}href") - self.assertEquals(len(hrefs), 2, - "expected two hrefs in response: %d received" \ - % len(hrefs)) + + self.assertEquals(len(hrefs), 4, + "expected 4 hrefs in response, got %d: %s" + % (len(hrefs), ", ".join([ x.text for x in hrefs ]))) self.assertEquals(hrefs[0].text, parentColl, "the first href is not a 'Calendar' parent coll.") - self.assertEquals(hrefs[1].text, resource, - "the 2nd href is not the accessible coll.") + resourceHrefs = { resource: False, + "%s.xml" % resource[:-1]: False, + "%s.ics" % resource[:-1]: False } + for href in hrefs[1:]: + self.assertTrue(resourceHrefs.has_key(href.text), + "received unexpected href: %s" % href.text) + self.assertFalse(resourceHrefs[href.text], + "href was returned more than once: %s" % href.text) + resourceHrefs[href.text] = True anonParentColl = '/SOGo/dav/public/%s/Calendar/' % username anon_propfind = webdavlib.WebDAVPROPFIND(anonParentColl, @@ -898,13 +1059,23 @@ class DAVCalendarPublicAclTest(unittest.TestCase): self.anon_client.execute(anon_propfind) hrefs = anon_propfind.response["document"] \ .findall("{DAV:}response/{DAV:}href") - self.assertEquals(len(hrefs), 2, "expected 2 hrefs in response") + + + self.assertEquals(len(hrefs), 4, + "expected 4 hrefs in response, got %d: %s" + % (len(hrefs), ", ".join([ x.text for x in hrefs ]))) self.assertEquals(hrefs[0].text, anonParentColl, "the first href is not a 'Calendar' parent coll.") anonResource = '%stest-dav-acl/' % anonParentColl - self.assertEquals(hrefs[1].text, anonResource, - "expected href '%s' instead of '%s'."\ - % (anonResource, hrefs[1].text)) + resourceHrefs = { anonResource: False, + "%s.xml" % anonResource[:-1]: False, + "%s.ics" % anonResource[:-1]: False } + for href in hrefs[1:]: + self.assertTrue(resourceHrefs.has_key(href.text), + "received unexpected href: %s" % href.text) + self.assertFalse(resourceHrefs[href.text], + "href was returned more than once: %s" % href.text) + resourceHrefs[href.text] = True self.subscriber_client.execute(propfind) hrefs = propfind.response["document"] \ diff --git a/Tests/Integration/test-put.py b/Tests/Stress/put.py similarity index 100% rename from Tests/Integration/test-put.py rename to Tests/Stress/put.py diff --git a/Tests/Stress/webdavsync.py b/Tests/Stress/webdavsync.py new file mode 100755 index 000000000..0939e7457 --- /dev/null +++ b/Tests/Stress/webdavsync.py @@ -0,0 +1,80 @@ +#!/usr/bin/python + +from config import hostname, port + +import webdavlib +import random +import time +import threading + +base=1127 +userscount=100 +password="" +batchcount=10 +sleeptime=3 +durationHeader="sogorequestduration" +#durationHeader="sogo-request-duration" + +class StressIteration(threading.Thread): + def __init__(self, username): + threading.Thread.__init__(self) + self.username = username + self.time = 0.0 + self.sogoTime = 0.0 + + def run(self): + client = webdavlib.WebDAVClient(hostname, port, + self.username, password) + resource = "/SOGo/dav/%s/Calendar/personal/" % self.username + startTime = time.time() + query = webdavlib.WebDAVSyncQuery(resource, None, + [ "getetag", "calendar-data" ]) + client.execute(query) + if query.response["status"] != 207: + print "*** received unexpected code: %d (%s)" \ + % (query.response["status"], resource) + endTime = time.time() + headers = query.response["headers"] + if headers.has_key(durationHeader): + self.sogoTime = float(headers[durationHeader]) + self.time = endTime - startTime + # print "%f, %f" % (self.time, self.sogoTime) + +class StressTest: + def __init__(self): + self.usernames = [ "invite%d" % (base + x) + for x in xrange(userscount) ] + self.random = random.Random() + + def iteration(self): + usernames = self.random.sample(self.usernames, batchcount) + startTime = time.time() + threads = [] + for username in usernames: + iteration = StressIteration(username) + iteration.start() + threads.append(iteration) + + for thread in threads: + thread.join() + + endTime = time.time() + + programTime = endTime - startTime + requestsTime = 0.0 + sogoTime = 0.0 + for thread in threads: + requestsTime = requestsTime + thread.time + sogoTime = sogoTime + thread.sogoTime + + print "Iteration time: %f, Total Requests Time: %f, Total SOGo Time: %f" \ + % (programTime, requestsTime, sogoTime) + + def start(self): + while True: + self.iteration() + time.sleep(sleeptime) + +if __name__ == "__main__": + test = StressTest() + test.start() diff --git a/debian/sogo.docs b/debian/sogo.docs index d46c591e6..6641930b8 100644 --- a/debian/sogo.docs +++ b/debian/sogo.docs @@ -1,4 +1,4 @@ NEWS README TODO -Scripts/sql-update-1.2.2_to_1.2.3.sh +Scripts/sql-update-1.2.2_to_1.3.0.sh diff --git a/sogo.spec b/sogo.spec index 5fa9612cd..4dc50837d 100644 --- a/sogo.spec +++ b/sogo.spec @@ -188,7 +188,7 @@ rm -fr ${RPM_BUILD_ROOT} %config %{_sysconfdir}/httpd/conf.d/SOGo.conf %config %{_sysconfdir}/sysconfig/sogo -%doc ChangeLog README NEWS Scripts/sql-update-20070724.sh Scripts/sql-update-20070822.sh Scripts/sql-update-20080303.sh Scripts/sql-update-101_to_102.sh Scripts/sql-update-1.2.2_to_1.2.3.sh +%doc ChangeLog README NEWS Scripts/sql-update-20070724.sh Scripts/sql-update-20070822.sh Scripts/sql-update-20080303.sh Scripts/sql-update-101_to_102.sh Scripts/sql-update-1.2.2_to_1.3.0.sh %files -n sogo-tool %{prefix}/Tools/Admin/sogo-tool