diff --git a/ChangeLog b/ChangeLog index 399fed97b..e52d0e557 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,9 +1,136 @@ +2010-06-02 Wolfgang Sourdeau + + * Tests/Integration/test-davacl.py (DAVPublicAccessTest): new test + class that implements tests strictly related to accessing the + "/public" prefix. + (DAVCalendarPublicAclTest.setUp): new test class that tests the + behaviour of calendar resources with regards to the super user, + a non-owner user, the "anonymous" user and the default user + (for default roles). + + * Tests/Integration/utilities.py (TestACLUtility.setupRights): we + excape the value for the "user" attribute with + xml.sax.saxutils.escape. + + * Tests/Integration/webdavlib.py (HTTPPUT.__init__): added the + "content_type" and "exclusive" optional parameters. The latter + implies the use of the "if-none-match" header. + (WebDAVPUT): removed useless operation class + + * SoObjects/SOGo/WORequest+SOGo.m (-handledByDefaultHandler): + fixed a bug where the -requestHandlerKey method would be invoked + on the "request" (NGHttpRequest) ivar rather than on self. + + * SoObjects/SOGo/SOGoUserFolder.m (_subFoldersFromFolder:): + thanks to the change below, the ACL checking code is no longer + needed here, where we can now concentrate on returning subfolders + metadata. + + * SoObjects/SOGo/SOGoParentFolder.m + (_fetchPersonalFolders:withChannel:): if the active user is not + the owner of the current parent folder, subfolders are returned + only when he/she has permissions set on them even for the + "personal" subfolder. + + * SoObjects/SOGo/SOGoGCSFolder.m (-aclsForUser:forObjectAtPath:): + extracted db code into a new "_realAclsForUser:forObjectAtPath:" + private method. When the "None" special role is returned, we + remove it from the list. Finally, we don't allow fetching default + roles when the specified uid is "anonymous". + + * SoObjects/Contacts/SOGoContactFolders.m (-appendSystemSources): + we now add public sources to the list of folders if and only if + the active user is the owner of the current parent folder. + + * SoObjects/SOGo/SOGoWebAuthenticator.m (-userInContext): if the + returned user has the login "anonymous", we return a corresponding + instance of SOGoUser to make sure methods are never invoked on + SoUser instances. + + * SoObjects/SOGo/SOGoUser.m (-rolesForObject:inContext:): now make + use of the new "isInPublicZone" method of SOGoObject to give the + "PublicUser" role to the unauthenticated user. + + * SoObjects/SOGo/SOGoObject.m (-isInPublicZone): same as below, + but we cache the result in a new "isInPublicZone" ivar. + + * Main/SOGo.m (-lookupName:inContext:acquire): when the "public" + key is requested in DAV mode, we now instantiate a + "SOGoPublicBaseFolder", if the "SOGoEnablePublicAccess" user + default is set. + (-isInPublicZone): new method that always returns "NO" and is + meant to be called from child objects. + + * SoObjects/SOGo/SOGoPublicBaseFolder.m: new class module that + enables anonymous access to resources. Instantiated from the + "SOGo" object. Implements a method "-isInPublicZone" that always + returns "YES". + 2010-06-02 Francis Lachapelle * UI/Scheduler/UIxCalListingActions.m (_userStateInEvent): fixed array indexes that would cause the user participation state to no longer be returned. +2010-06-01 Wolfgang Sourdeau + + * SoObjects/SOGo/WORequest+SOGo.m (-handledByDefaultHandler): test + the request handler key instead of invoking isSoWebDAVRequest, + which performs additional and permission tests. + + * Tests/Integration/test-davacl.py + (DAVCalendarAclTest._testRights): reenabled test code even when no + classification rights are available. + + * SoObjects/SOGo/SOGoSystemDefaults.m (-enablePublicAccess): new + accessor for the SOGoEnablePublicAccess boolean user default. + + * SoObjects/SOGo/SOGoUserManager.m + (-contactInfosForUserWithUIDorEmail:): return a fixed dictionary + of information for uid = "anonymous". + + * Tests/Integration/test-*.py: take the API changes in + webdavlib.py into accounts with regards to XPath queries. + + * SoObjects/Appointments/SOGoAppointmentFolder.m + (-_privacySqlString): renamed to the new standardized + "aclSQLListingFilter". Again, a difference is now made between + empty and nil return strings, which we now apply here too. + (-bareFetchFields:from:to:title:component:additionalFilters:, + (-fetchFields:from:to:title:component:additionalFilters:includeProtectedInformation:): + make use of the new semantics to the above method. + + * SoObjects/SOGo/SOGoGCSFolder.m (-aclSQLListingFilter): new + overridable method designed to return a filter to SQL queries that + avoids requesting records that are not meant to be visible to the + active user. Replaces and extends -[SOGoAppointmentFolder + _privacySqlString] with the additional convention that returning a + nil string will prevent any query at all. + (-toOneRelationshipKeys): new overriden method (see below). Make + use of the new aclSQLListingFilter method to retrieve records so + that unaccessible records are not even listed. + + * SoObjects/SOGo/SOGoFolder.m (-fetchContentObjectNames): removed + this useless alias to "toOneRelationshipKeys". + + * Tests/Integration/all.py: disable multilanguage tests by + default, reverting the "disbale-languages" cmd-line parameter to + "enable-languages". + + * Tests/Integration/webdavlib.py (WebDAVClient.__init__): made + "username" and "password" parameters optional to enable anonymous + connections. Also, M2Crypto.httpslib is imported explicitly only + when an SSL connection is requested. This renders this library + optional. + (WebDAVQuery.set_response): we now make use of the xml.etree API + instead of the one from xml.dom. + (xpath_evaluate): the above change makes this method obsolete + since XPath queries can now be performed directly on returned + elements. + + * SoObjects/SOGo/SOGoCache.m (-setValue:forKey:expire:): display + the memcached error string when an error occurs. + 2010-05-28 Francis Lachapelle * UI/MailerUI/UIxMailMainFrame.m (-saveColumnsStateAction): new diff --git a/Main/SOGo.m b/Main/SOGo.m index 817eb395e..78b884745 100644 --- a/Main/SOGo.m +++ b/Main/SOGo.m @@ -49,6 +49,7 @@ #import #import #import +#import #import #import #import @@ -305,18 +306,21 @@ static BOOL debugLeaks; id obj; WORequest *request; BOOL isDAVRequest; + SOGoSystemDefaults *sd; /* put locale info into the context in case it's not there */ [self _setupLocaleInContext:_ctx]; + sd = [SOGoSystemDefaults sharedSystemDefaults]; request = [_ctx request]; - isDAVRequest = [request isSoWebDAVRequest]; - if (isDAVRequest - || [[SOGoSystemDefaults sharedSystemDefaults] isWebAccessEnabled]) + isDAVRequest = [[request requestHandlerKey] isEqualToString:@"dav"]; + if (isDAVRequest || [sd isWebAccessEnabled]) { if (isDAVRequest) { - if ([[request method] isEqualToString: @"REPORT"]) + if ([_key isEqualToString: @"public"] && [sd enablePublicAccess]) + obj = [SOGoPublicBaseFolder objectWithName: @"public" inContainer: self]; + else if ([[request method] isEqualToString: @"REPORT"]) obj = [self davReportInvocationForKey: _key]; else obj = nil; @@ -326,6 +330,7 @@ static BOOL debugLeaks; /* first check attributes directly bound to the application */ obj = [super lookupName:_key inContext:_ctx acquire:_flag]; } + if (!obj) { /* @@ -335,7 +340,6 @@ static BOOL debugLeaks; Addition: we also get queries for various other methods, like "GET" if no method was provided in the query path. */ - if ([_key length] > 0 && ![_key isEqualToString:@"favicon.ico"]) obj = [self lookupUser: _key inContext: _ctx]; } @@ -346,6 +350,11 @@ static BOOL debugLeaks; return obj; } +- (BOOL) isInPublicZone +{ + return NO; +} + /* WebDAV */ - (NSString *) davDisplayName @@ -422,7 +431,7 @@ static BOOL debugLeaks; [self logWithFormat: @"request took %f seconds to execute", timeDelta]; [resp setHeader: [NSString stringWithFormat: @"%f", timeDelta] - forKey: @"SOGoRequestDuration"]; + forKey: @"SOGo-Request-Duration"]; } if (![self isTerminating]) diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index 6726f5754..ee1a9d3af 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -444,70 +444,41 @@ static NSNumber *sharedYes = nil; end, start]; } -- (NSString *) _privacyClassificationStringsForUID: (NSString *) uid +- (NSString *) aclSQLListingFilter { - NSMutableString *classificationString; - NSString *currentRole; - unsigned int counter; - iCalAccessClass classes[] = {iCalAccessPublic, iCalAccessPrivate, - iCalAccessConfidential}; - - classificationString = [NSMutableString string]; - for (counter = 0; counter < 3; counter++) - { - currentRole = [self roleForComponentsWithAccessClass: classes[counter] - forUser: uid]; - if ([currentRole length] > 0) - [classificationString appendFormat: @"c_classification = %d or ", - classes[counter]]; - } - - return classificationString; -} - -- (NSString *) _privacySqlString -{ - NSString *privacySqlString, *login; + NSString *filter; NSMutableArray *grantedClasses, *deniedClasses; NSNumber *classNumber; unsigned int grantedCount; iCalAccessClass currentClass; - login = [[context activeUser] login]; - if ([login isEqualToString: @"freebusy"]) - privacySqlString = @"c_isopaque = 1"; - else + [self initializeQuickTablesAclsInContext: context]; + grantedClasses = [NSMutableArray arrayWithCapacity: 3]; + deniedClasses = [NSMutableArray arrayWithCapacity: 3]; + for (currentClass = 0; + currentClass < iCalAccessClassCount; currentClass++) { - [self initializeQuickTablesAclsInContext: context]; - grantedClasses = [NSMutableArray arrayWithCapacity: 3]; - deniedClasses = [NSMutableArray arrayWithCapacity: 3]; - for (currentClass = 0; - currentClass < iCalAccessClassCount; currentClass++) - { - classNumber = [NSNumber numberWithInt: currentClass]; - if (userCanAccessObjectsClassifiedAs[currentClass]) - [grantedClasses addObject: classNumber]; - else - [deniedClasses addObject: classNumber]; - } - grantedCount = [grantedClasses count]; - if (grantedCount == 3) - privacySqlString = @""; - else if (grantedCount == 2) - privacySqlString - = [NSString stringWithFormat: @"c_classification != %@", - [deniedClasses objectAtIndex: 0]]; - else if (grantedCount == 1) - privacySqlString - = [NSString stringWithFormat: @"c_classification = %@", - [grantedClasses objectAtIndex: 0]]; + classNumber = [NSNumber numberWithInt: currentClass]; + if (userCanAccessObjectsClassifiedAs[currentClass]) + [grantedClasses addObject: classNumber]; else - /* We prevent any event/task from being listed. There must be a better - way... */ - privacySqlString = @"c_classification = 255"; + [deniedClasses addObject: classNumber]; } + grantedCount = [grantedClasses count]; + if (grantedCount == 3) + filter = @""; + else if (grantedCount == 2) + filter + = [NSString stringWithFormat: @"c_classification != %@", + [deniedClasses objectAtIndex: 0]]; + else if (grantedCount == 1) + filter + = [NSString stringWithFormat: @"c_classification = %@", + [grantedClasses objectAtIndex: 0]]; + else + filter = nil; - return privacySqlString; + return filter; } - (NSArray *) bareFetchFields: (NSArray *) fields @@ -520,8 +491,9 @@ static NSNumber *sharedYes = nil; EOQualifier *qualifier; GCSFolder *folder; NSString *sql, *dateSqlString, *titleSqlString, *componentSqlString, - *privacySqlString; + *privacySQLString; NSMutableString *filterSqlString; + NSArray *records; folder = [self ocsFolder]; if (startDate && endDate) @@ -544,25 +516,32 @@ static NSNumber *sharedYes = nil; if ([filters length]) [filterSqlString appendFormat: @"AND (%@)", filters]; - privacySqlString = [self _privacySqlString]; - if ([privacySqlString length]) - [filterSqlString appendFormat: @"AND (%@)", privacySqlString]; + privacySQLString = [self aclSQLListingFilter]; + if (privacySQLString) + { + if ([privacySQLString length]) + [filterSqlString appendFormat: @"AND (%@)", privacySQLString]; - /* prepare mandatory fields */ + /* prepare mandatory fields */ - sql = [NSString stringWithFormat: @"%@%@%@%@", - dateSqlString, titleSqlString, componentSqlString, - filterSqlString]; - /* sql is empty when we fetch everything (all parameters are nil) */ - if ([sql length] > 0) - sql = [sql substringFromIndex: 4]; + sql = [NSString stringWithFormat: @"%@%@%@%@", + dateSqlString, titleSqlString, componentSqlString, + filterSqlString]; + /* sql is empty when we fetch everything (all parameters are nil) */ + if ([sql length] > 0) + sql = [sql substringFromIndex: 4]; + else + sql = nil; + + /* fetch non-recurrent apts first */ + qualifier = [EOQualifier qualifierWithQualifierFormat: sql]; + + records = [folder fetchFields: fields matchingQualifier: qualifier]; + } else - sql = nil; - - /* fetch non-recurrent apts first */ - qualifier = [EOQualifier qualifierWithQualifierFormat: sql]; + records = [NSArray array]; - return [folder fetchFields: fields matchingQualifier: qualifier]; + return records; } - (BOOL) _checkIfWeCanRememberRecords: (NSArray *) fields @@ -965,7 +944,7 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir NSMutableArray *fields, *ma; NSArray *records; NSMutableString *baseWhere; - NSString *where, *dateSqlString, *privacySqlString, *currentLogin; + NSString *where, *dateSqlString, *privacySQLString, *currentLogin; NSCalendarDate *endDate; NGCalendarDateRange *r; BOOL rememberRecords, canCycle; @@ -1005,76 +984,83 @@ firstInstanceCalendarDateRange: (NGCalendarDateRange *) fir dateSqlString = @""; } - privacySqlString = [self _privacySqlString]; - if ([privacySqlString length]) - [baseWhere appendFormat: @"AND %@", privacySqlString]; - - if ([title length]) - [baseWhere appendFormat: @"AND c_title isCaseInsensitiveLike: '%%%@%%'", - [title stringByReplacingString: @"'" withString: @"\\'\\'"]]; - - if ([filters length]) - [baseWhere appendFormat: @"AND (%@)", filters]; - - /* prepare mandatory fields */ - - fields = [NSMutableArray arrayWithArray: _fields]; - [fields addObjectUniquely: @"c_name"]; - [fields addObjectUniquely: @"c_uid"]; - [fields addObjectUniquely: @"c_startdate"]; - [fields addObjectUniquely: @"c_enddate"]; - [fields addObjectUniquely: @"c_isallday"]; - - if (canCycle) - where = [NSString stringWithFormat: @"%@ %@ AND c_iscycle = 0", - baseWhere, dateSqlString]; - else - where = baseWhere; - - /* fetch non-recurrent apts first */ - qualifier = [EOQualifier qualifierWithQualifierFormat: - [where substringFromIndex: 4]]; - records = [folder fetchFields: fields matchingQualifier: qualifier]; - if (records) + privacySQLString = [self aclSQLListingFilter]; + if (privacySQLString) { - if (r) - records = [self fixupRecords: records]; - ma = [NSMutableArray arrayWithArray: records]; - } - else - ma = nil; + if ([privacySQLString length]) + [baseWhere appendFormat: @"AND %@", privacySQLString]; - /* fetch recurrent apts now. we do NOT consider events with no cycle end. */ -// || _endDate || filters) - if (canCycle && _endDate) - { - where = [NSString stringWithFormat: @"%@ AND c_iscycle = 1", baseWhere]; - qualifier = [EOQualifier qualifierWithQualifierFormat: [where substringFromIndex: 4]]; + if ([title length]) + [baseWhere + appendFormat: @"AND c_title isCaseInsensitiveLike: '%%%@%%'", + [title stringByReplacingString: @"'" withString: @"\\'\\'"]]; + + if ([filters length]) + [baseWhere appendFormat: @"AND (%@)", filters]; + + /* prepare mandatory fields */ + + fields = [NSMutableArray arrayWithArray: _fields]; + [fields addObjectUniquely: @"c_name"]; + [fields addObjectUniquely: @"c_uid"]; + [fields addObjectUniquely: @"c_startdate"]; + [fields addObjectUniquely: @"c_enddate"]; + [fields addObjectUniquely: @"c_isallday"]; + + if (canCycle) + where = [NSString stringWithFormat: @"%@ %@ AND c_iscycle = 0", + baseWhere, dateSqlString]; + else + where = baseWhere; + + /* fetch non-recurrent apts first */ + qualifier = [EOQualifier qualifierWithQualifierFormat: + [where substringFromIndex: 4]]; records = [folder fetchFields: fields matchingQualifier: qualifier]; if (records) { if (r) - records = [self _flattenCycleRecords: records fetchRange: r]; - if (ma) - [ma addObjectsFromArray: records]; - else - ma = [NSMutableArray arrayWithArray: records]; + records = [self fixupRecords: records]; + ma = [NSMutableArray arrayWithArray: records]; } - } - if (!ma) - { - [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__]; - return nil; - } + else + ma = nil; - currentLogin = [[context activeUser] login]; - if (![currentLogin isEqualToString: owner] && !_includeProtectedInformation) - [self _fixupProtectedInformation: [ma objectEnumerator] - inFields: _fields - forUser: currentLogin]; + /* fetch recurrent apts now. we do NOT consider events with no cycle + end. */ + if (canCycle && _endDate) + { + where = [NSString stringWithFormat: @"%@ AND c_iscycle = 1", baseWhere]; + qualifier = [EOQualifier qualifierWithQualifierFormat: [where substringFromIndex: 4]]; + records = [folder fetchFields: fields matchingQualifier: qualifier]; + if (records) + { + if (r) + records = [self _flattenCycleRecords: records fetchRange: r]; + if (ma) + [ma addObjectsFromArray: records]; + else + ma = [NSMutableArray arrayWithArray: records]; + } + } + if (!ma) + { + [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__]; + return nil; + } - if (rememberRecords) - [self _rememberRecords: ma]; + currentLogin = [[context activeUser] login]; + if (![currentLogin isEqualToString: owner] + && !_includeProtectedInformation) + [self _fixupProtectedInformation: [ma objectEnumerator] + inFields: _fields + forUser: currentLogin]; + + if (rememberRecords) + [self _rememberRecords: ma]; + } + else + ma = [NSMutableArray array]; return ma; } diff --git a/SoObjects/Appointments/product.plist b/SoObjects/Appointments/product.plist index 22df3dfcd..865942b97 100644 --- a/SoObjects/Appointments/product.plist +++ b/SoObjects/Appointments/product.plist @@ -100,8 +100,8 @@ superclass = "SOGoContentObject"; protectedBy = "Access Contents Information"; defaultRoles = { - "Access Contents Information" = ( "Owner", "Authenticated" ); - "WebDAV Access" = ( "Owner", "Authenticated" ); + "Access Contents Information" = ( "Owner", "Authenticated", "PublicUser" ); + "WebDAV Access" = ( "Owner", "Authenticated", "PublicUser" ); }; }; }; diff --git a/SoObjects/Contacts/SOGoContactFolders.m b/SoObjects/Contacts/SOGoContactFolders.m index 105e6149e..98197dfa6 100644 --- a/SoObjects/Contacts/SOGoContactFolders.m +++ b/SoObjects/Contacts/SOGoContactFolders.m @@ -58,18 +58,25 @@ NSEnumerator *sourceIDs; NSString *currentSourceID, *srcDisplayName, *domain; SOGoContactSourceFolder *currentFolder; + SOGoUser *currentUser; - domain = [[context activeUser] domain]; - um = [SOGoUserManager sharedUserManager]; - sourceIDs = [[um addressBookSourceIDsInDomain: domain] objectEnumerator]; - while ((currentSourceID = [sourceIDs nextObject])) + currentUser = [context activeUser]; + if (activeUserIsOwner + || [[currentUser login] + isEqualToString: [self ownerInContext: context]]) { - srcDisplayName = [um displayNameForSourceWithID: currentSourceID]; - currentFolder = [SOGoContactSourceFolder folderWithName: currentSourceID - andDisplayName: srcDisplayName - inContainer: self]; - [currentFolder setSource: [um sourceWithID: currentSourceID]]; - [subFolders setObject: currentFolder forKey: currentSourceID]; + domain = [currentUser domain]; + um = [SOGoUserManager sharedUserManager]; + sourceIDs = [[um addressBookSourceIDsInDomain: domain] objectEnumerator]; + while ((currentSourceID = [sourceIDs nextObject])) + { + srcDisplayName = [um displayNameForSourceWithID: currentSourceID]; + currentFolder = [SOGoContactSourceFolder folderWithName: currentSourceID + andDisplayName: srcDisplayName + inContainer: self]; + [currentFolder setSource: [um sourceWithID: currentSourceID]]; + [subFolders setObject: currentFolder forKey: currentSourceID]; + } } return nil; diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index 58c0970c6..4a31afd9a 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -86,6 +86,7 @@ SOGo_OBJC_FILES = \ SOGoFolder.m \ SOGoGCSFolder.m \ SOGoParentFolder.m \ + SOGoPublicBaseFolder.m \ SOGoUserFolder.m \ \ SOGoDefaultsSource.m \ diff --git a/SoObjects/SOGo/SOGoCache.m b/SoObjects/SOGo/SOGoCache.m index 8415473cd..9d6da8159 100644 --- a/SoObjects/SOGo/SOGoCache.m +++ b/SoObjects/SOGo/SOGoCache.m @@ -233,8 +233,8 @@ static memcached_st *handle = NULL; expiration, 0); if (error != MEMCACHED_SUCCESS) [self logWithFormat: - @"memcached error: unable to cache values for key '%@'", - key]; + @"an error occurred when caching value for key '%@':" + @" \"%s\"", key, memcached_strerror(handle, error)]; //else //[self logWithFormat: @"memcached: cached values (%s) with subtype %@ //for user %@", value, theType, theLogin]; diff --git a/SoObjects/SOGo/SOGoFolder.h b/SoObjects/SOGo/SOGoFolder.h index e2a6e927e..24de0805b 100644 --- a/SoObjects/SOGo/SOGoFolder.h +++ b/SoObjects/SOGo/SOGoFolder.h @@ -40,7 +40,6 @@ - (NSString *) realNameInContainer; - (NSString *) folderType; -- (NSArray *) fetchContentObjectNames; - (BOOL) isValidContentName: (NSString *) name; diff --git a/SoObjects/SOGo/SOGoFolder.m b/SoObjects/SOGo/SOGoFolder.m index 787c963d7..32fb8a614 100644 --- a/SoObjects/SOGo/SOGoFolder.m +++ b/SoObjects/SOGo/SOGoFolder.m @@ -144,15 +144,9 @@ return nil; } -#warning we should remove this method - (NSArray *) toOneRelationshipKeys { - return [self fetchContentObjectNames]; -} - -- (NSArray *) fetchContentObjectNames -{ - return [NSArray array]; + return nil; } - (NSArray *) toManyRelationshipKeys diff --git a/SoObjects/SOGo/SOGoGCSFolder.h b/SoObjects/SOGo/SOGoGCSFolder.h index 059a4bf99..76ee7185c 100644 --- a/SoObjects/SOGo/SOGoGCSFolder.h +++ b/SoObjects/SOGo/SOGoGCSFolder.h @@ -85,8 +85,6 @@ - (id) createChildComponentWithName: (NSString *) newName andContent: (NSString *) newContent; -- (NSArray *) fetchContentObjectNames; - /* folder type */ - (BOOL) folderIsMandatory; diff --git a/SoObjects/SOGo/SOGoGCSFolder.m b/SoObjects/SOGo/SOGoGCSFolder.m index c51901a96..9e8039802 100644 --- a/SoObjects/SOGo/SOGoGCSFolder.m +++ b/SoObjects/SOGo/SOGoGCSFolder.m @@ -550,24 +550,63 @@ static NSArray *childRecordFields = nil; [self _subscriberRenameTo: newName]; } -- (NSArray *) fetchContentObjectNames +- (NSString *) aclSQLListingFilter +{ + NSString *filter, *login; + NSArray *roles; + + login = [[context activeUser] login]; + if (activeUserIsOwner + || [[self ownerInContext: nil] isEqualToString: login]) + filter = @""; + else + { + roles = [self aclsForUser: login]; + if ([roles containsObject: SOGoRole_ObjectViewer] + || [roles containsObject: SOGoRole_ObjectEditor]) + filter = @""; + else + 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; +} + +- (NSArray *) toOneRelationshipKeys { NSArray *records, *names; - - records = [[self ocsFolder] fetchFields: childRecordFields - matchingQualifier:nil]; - if (![records isNotNull]) - { - [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__]; - return nil; - } - if ([records isKindOfClass: [NSException class]]) - return records; + NSString *sqlFilter; + EOQualifier *qualifier; - [childRecords release]; - names = [records objectsForKey: @"c_name" notFoundMarker: nil]; - childRecords = [[NSMutableDictionary alloc] initWithObjects: records - forKeys: names]; + sqlFilter = [self aclSQLListingFilter]; + if (sqlFilter) + { + if ([sqlFilter length] > 0) + qualifier = [EOQualifier qualifierWithQualifierFormat: sqlFilter]; + else + qualifier = nil; + + records = [[self ocsFolder] fetchFields: childRecordFields + matchingQualifier: qualifier]; + if (![records isNotNull]) + { + [self errorWithFormat: @"(%s): fetch failed!", __PRETTY_FUNCTION__]; + return nil; + } + if ([records isKindOfClass: [NSException class]]) + return records; + + names = [records objectsForKey: @"c_name" notFoundMarker: nil]; + + [childRecords release]; + childRecords = [[NSMutableDictionary alloc] initWithObjects: records + forKeys: names]; + } + else + names = [NSArray array]; return names; } @@ -1345,13 +1384,12 @@ static NSArray *childRecordFields = nil; [aclsForObject removeObjectForKey: uid]; } -- (NSArray *) aclsForUser: (NSString *) uid - forObjectAtPath: (NSArray *) objectPathArray +- (NSArray *) _realAclsForUser: (NSString *) uid + forObjectAtPath: (NSArray *) objectPathArray { NSArray *acls; - NSString *objectPath, *module; + NSString *objectPath; NSDictionary *aclsForObject; - SOGoDomainDefaults *dd; objectPath = [objectPathArray componentsJoinedByString: @"/"]; aclsForObject = [aclCache objectForKey: objectPath]; @@ -1362,13 +1400,26 @@ static NSArray *childRecordFields = nil; if (!acls) { acls = [self _fetchAclsForUser: uid forObjectAtPath: objectPath]; - if (!acls) + if (!acls + || ([acls count] == 1 && [acls containsObject: SOGoRole_None])) acls = [NSArray array]; [self _cacheRoles: acls forUser: uid forObjectAtPath: objectPath]; } - if (!([acls count] || [uid isEqualToString: defaultUserID])) - acls = [self aclsForUser: defaultUserID forObjectAtPath: objectPathArray]; + return acls; +} + +- (NSArray *) aclsForUser: (NSString *) uid + forObjectAtPath: (NSArray *) objectPathArray +{ + NSArray *acls; + NSString *module; + SOGoDomainDefaults *dd; + + acls = [self _realAclsForUser: uid forObjectAtPath: objectPathArray]; + if (!([acls count] || [uid isEqualToString: @"anonymous"])) + acls = [self _realAclsForUser: defaultUserID + forObjectAtPath: objectPathArray]; // If we still don't have ACLs defined for this particular resource, // let's go get the domain defaults, if any. @@ -1472,6 +1523,9 @@ static NSArray *childRecordFields = nil; forObjectAtPath: objectPathArray]; newRoles = [NSMutableArray arrayWithArray: roles]; + [newRoles removeObject: SoRole_Authenticated]; + [newRoles removeObject: SoRole_Anonymous]; + [newRoles removeObject: SOGoRole_PublicUser]; [newRoles removeObject: SOGoRole_AuthorizedSubscriber]; [newRoles removeObject: SOGoRole_None]; objectPath = [objectPathArray componentsJoinedByString: @"/"]; diff --git a/SoObjects/SOGo/SOGoObject.h b/SoObjects/SOGo/SOGoObject.h index 82bb0cfc2..2daaa8abb 100644 --- a/SoObjects/SOGo/SOGoObject.h +++ b/SoObjects/SOGo/SOGoObject.h @@ -68,6 +68,7 @@ SOGoWebDAVAclManager *webdavAclManager; id container; BOOL activeUserIsOwner; + BOOL isInPublicZone; } + (NSString *) globallyUniqueObjectId; @@ -79,6 +80,8 @@ + (SOGoWebDAVAclManager *) webdavAclManager; +- (BOOL) isInPublicZone; + /* accessors */ - (NSString *) nameInContainer; diff --git a/SoObjects/SOGo/SOGoObject.m b/SoObjects/SOGo/SOGoObject.m index 06b4ce34f..da6547c84 100644 --- a/SoObjects/SOGo/SOGoObject.m +++ b/SoObjects/SOGo/SOGoObject.m @@ -166,6 +166,7 @@ owner = nil; webdavAclManager = [[self class] webdavAclManager]; activeUserIsOwner = NO; + isInPublicZone = NO; } return self; @@ -228,6 +229,14 @@ return owner; } +- (BOOL) isInPublicZone +{ + if (!isInPublicZone) + isInPublicZone = [container isInPublicZone]; + + return isInPublicZone; +} + /* hierarchy */ - (NSArray *) fetchSubfolders diff --git a/SoObjects/SOGo/SOGoParentFolder.m b/SoObjects/SOGo/SOGoParentFolder.m index 0f31d89a0..2066ba3fe 100644 --- a/SoObjects/SOGo/SOGoParentFolder.m +++ b/SoObjects/SOGo/SOGoParentFolder.m @@ -175,10 +175,12 @@ static SoSecurityManager *sm = nil; { NSArray *attrs; NSDictionary *row; - BOOL hasPersonal; + BOOL hasPersonal, ignoreRights; SOGoGCSFolder *folder; - NSString *key; + NSString *key, *login; NSException *error; + SOGoUser *currentUser; + SoSecurityManager *securityManager; if (!subFolderClass) subFolderClass = [[self class] subFolderClass]; @@ -187,23 +189,33 @@ static SoSecurityManager *sm = nil; error = [fc evaluateExpressionX: sql]; if (!error) { + currentUser = [context activeUser]; + login = [currentUser login]; + ignoreRights = (activeUserIsOwner || [login isEqualToString: owner] + || [currentUser isSuperUser]); + if (!ignoreRights) + securityManager = [SoSecurityManager sharedSecurityManager]; + attrs = [fc describeResults: NO]; - row = [fc fetchAttributes: attrs withZone: NULL]; - while (row) + while ((row = [fc fetchAttributes: attrs withZone: NULL])) { key = [row objectForKey: @"c_path4"]; if ([key isKindOfClass: [NSString class]]) { folder = [subFolderClass objectWithName: key inContainer: self]; - hasPersonal = (hasPersonal || [key isEqualToString: @"personal"]); + hasPersonal = (hasPersonal + || [key isEqualToString: @"personal"]); [folder setOCSPath: [NSString stringWithFormat: @"%@/%@", OCSPath, key]]; + if (ignoreRights + || ![securityManager validatePermission: SOGoPerm_AccessObject + onObject: folder + inContext: context]) [subFolders setObject: folder forKey: key]; } - row = [fc fetchAttributes: attrs withZone: NULL]; } - if (!hasPersonal) + if (ignoreRights && !hasPersonal) [self _createPersonalFolder]; } @@ -382,11 +394,6 @@ static SoSecurityManager *sm = nil; return error; } -- (NSArray *) fetchContentObjectNames -{ - return nil; -} - - (id) lookupName: (NSString *) name inContext: (WOContext *) lookupContext acquire: (BOOL) acquire diff --git a/SoObjects/SOGo/SOGoPermissions.h b/SoObjects/SOGo/SOGoPermissions.h index 6b1a4990a..124408d87 100644 --- a/SoObjects/SOGo/SOGoPermissions.h +++ b/SoObjects/SOGo/SOGoPermissions.h @@ -37,6 +37,7 @@ extern NSString *SOGoRole_FolderEraser; extern NSString *SOGoRole_FolderViewer; extern NSString *SOGoRole_AuthorizedSubscriber; +extern NSString *SOGoRole_PublicUser; extern NSString *SOGoRole_None; extern NSString *SOGoMailRole_SeenKeeper; diff --git a/SoObjects/SOGo/SOGoPermissions.m b/SoObjects/SOGo/SOGoPermissions.m index b6e0bb4f8..9a66d8888 100644 --- a/SoObjects/SOGo/SOGoPermissions.m +++ b/SoObjects/SOGo/SOGoPermissions.m @@ -32,6 +32,7 @@ NSString *SOGoRole_FolderCreator = @"FolderCreator"; NSString *SOGoRole_FolderEraser = @"FolderEraser"; NSString *SOGoRole_AuthorizedSubscriber = @"AuthorizedSubscriber"; +NSString *SOGoRole_PublicUser = @"PublicUser"; NSString *SOGoRole_None = @"None"; /* Calendar */ diff --git a/SoObjects/SOGo/SOGoPublicBaseFolder.h b/SoObjects/SOGo/SOGoPublicBaseFolder.h new file mode 100644 index 000000000..e75221936 --- /dev/null +++ b/SoObjects/SOGo/SOGoPublicBaseFolder.h @@ -0,0 +1,31 @@ +/* SOGoPublicBaseFolder.h - this file is part of SOGo + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef SOGOPUBLICBASEFOLDER_H +#define SOGOPUBLICBASEFOLDER_H + +#import "SOGoFolder.h" + +@interface SOGoPublicBaseFolder : SOGoFolder +@end + +#endif /* SOGOPUBLICBASEFOLDER_H */ diff --git a/SoObjects/SOGo/SOGoPublicBaseFolder.m b/SoObjects/SOGo/SOGoPublicBaseFolder.m new file mode 100644 index 000000000..7f9c7a33e --- /dev/null +++ b/SoObjects/SOGo/SOGoPublicBaseFolder.m @@ -0,0 +1,50 @@ +/* SOGoPublicBaseFolder.m - this file is part of SOGo + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import + +#import "SOGoUser.h" + +#import "SOGoPublicBaseFolder.h" + +@implementation SOGoPublicBaseFolder + +- (id) lookupName: (NSString *) key + inContext: (id) localContext + acquire: (BOOL) acquire +{ + id userFolder; + + if ([key length] > 0 && [SOGoUser userWithLogin: key roles: nil]) + userFolder = [SOGoUserFolder objectWithName: key inContainer: self]; + else + userFolder = nil; + + return userFolder; +} + +- (BOOL) isInPublicZone +{ + return YES; +} + +@end diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 1752eb8e9..e5cc39534 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -64,6 +64,8 @@ - (NSString *) CASServiceURL; +- (BOOL) enablePublicAccess; + @end #endif /* SOGOSYSTEMDEFAULTS_H */ diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index 5849ccd77..5825285de 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -296,4 +296,9 @@ BootstrapNSUserDefaults () return [self stringForKey: @"SOGoCASServiceURL"]; } +- (BOOL) enablePublicAccess +{ + return [self boolForKey: @"SOGoEnablePublicAccess"]; +} + @end diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index 88be4c5b6..9d124b7c1 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -650,6 +650,9 @@ sogoRoles = [(SOGoObject *) object subscriptionRoles]; if ([sogoRoles firstObjectCommonWithArray: rolesForObject]) [rolesForObject addObject: SOGoRole_AuthorizedSubscriber]; + if ([login isEqualToString: @"anonymous"] + && [(SOGoObject *) object isInPublicZone]) + [rolesForObject addObject: SOGoRole_PublicUser]; } #warning this is a hack to work-around the poor implementation of PROPPATCH in SOPE diff --git a/SoObjects/SOGo/SOGoUserFolder.m b/SoObjects/SOGo/SOGoUserFolder.m index 91c10f532..0ed2c4664 100644 --- a/SoObjects/SOGo/SOGoUserFolder.m +++ b/SoObjects/SOGo/SOGoUserFolder.m @@ -27,7 +27,6 @@ #import #import -#import #import #import #import @@ -148,35 +147,28 @@ NSMutableArray *folders; NSEnumerator *subfolders; SOGoFolder *currentFolder; - NSString *folderName, *folderOwner; + NSString *folderName; + Class subfolderClass; NSMutableDictionary *currentDictionary; - SoSecurityManager *securityManager; - - folderOwner = [parentFolder ownerInContext: context]; - securityManager = [SoSecurityManager sharedSecurityManager]; folders = [NSMutableArray array]; + subfolderClass = [[parentFolder class] subFolderClass]; + subfolders = [[parentFolder subFolders] objectEnumerator]; while ((currentFolder = [subfolders nextObject])) { - if (![securityManager validatePermission: SOGoPerm_AccessObject - onObject: currentFolder inContext: context] - && [[currentFolder ownerInContext: context] - isEqualToString: folderOwner] - && [NSStringFromClass([currentFolder class]) compare: @"SOGoWebAppointmentFolder"] != NSOrderedSame) + if ([currentFolder isMemberOfClass: subfolderClass]) { folderName = [NSString stringWithFormat: @"/%@/%@", [parentFolder nameInContainer], [currentFolder nameInContainer]]; - currentDictionary - = [NSMutableDictionary dictionaryWithCapacity: 3]; + currentDictionary = [NSMutableDictionary dictionaryWithCapacity: 4]; [currentDictionary setObject: [currentFolder displayName] - forKey: @"displayName"]; + forKey: @"displayName"]; [currentDictionary setObject: folderName forKey: @"name"]; - [currentDictionary setObject: folderOwner forKey: @"owner"]; [currentDictionary setObject: [currentFolder folderType] - forKey: @"type"]; + forKey: @"type"]; [folders addObject: currentDictionary]; } } @@ -587,7 +579,7 @@ /* WebDAV */ -- (NSArray *) fetchContentObjectNames +- (NSArray *) toOneRelationshipKeys { SOGoSystemDefaults *sd; SOGoUser *currentUser; diff --git a/SoObjects/SOGo/SOGoUserManager.m b/SoObjects/SOGo/SOGoUserManager.m index 9ade70d4f..7414d4e03 100644 --- a/SoObjects/SOGo/SOGoUserManager.m +++ b/SoObjects/SOGo/SOGoUserManager.m @@ -593,17 +593,39 @@ forLogin: key]; } +- (NSMutableDictionary *) _contactInfosForAnonymous +{ + static NSMutableDictionary *user = nil; + + if (!user) + { + user = [[NSMutableDictionary alloc] initWithCapacity: 7]; + [user setObject: [NSArray arrayWithObject: @"anonymous"] + forKey: @"emails"]; + [user setObject: @"Public User" forKey: @"cn"]; + [user setObject: @"anonymous" forKey: @"c_uid"]; + [user setObject: @"" forKey: @"c_domain"]; + [user setObject: [NSNumber numberWithBool: YES] + forKey: @"CalendarAccess"]; + [user setObject: [NSNumber numberWithBool: NO] + forKey: @"MailAccess"]; + } + + return user; +} + - (NSDictionary *) contactInfosForUserWithUIDorEmail: (NSString *) uid { - NSMutableDictionary *currentUser, *contactInfos; + NSMutableDictionary *currentUser; NSString *aUID, *jsonUser; BOOL newUser; - if ([uid length] > 0) + if ([uid isEqualToString: @"anonymous"]) + currentUser = [self _contactInfosForAnonymous]; + else if ([uid length] > 0) { // Remove the "@" prefix used to identified groups in the ACL tables. aUID = [uid hasPrefix: @"@"] ? [uid substringFromIndex: 1] : uid; - contactInfos = [NSMutableDictionary dictionary]; jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: aUID]; currentUser = [NSMutableDictionary dictionaryWithJSONString: jsonUser]; if (!([currentUser objectForKey: @"emails"] diff --git a/SoObjects/SOGo/SOGoWebAuthenticator.m b/SoObjects/SOGo/SOGoWebAuthenticator.m index a5eea8f82..70d9a71f0 100644 --- a/SoObjects/SOGo/SOGoWebAuthenticator.m +++ b/SoObjects/SOGo/SOGoWebAuthenticator.m @@ -110,7 +110,7 @@ SOGoUser *user; user = (SOGoUser *) [super userInContext: _ctx]; - if (!user) + if (!user || [[user login] isEqualToString: @"anonymous"]) { if (!anonymous) anonymous = [[SOGoUser alloc] diff --git a/SoObjects/SOGo/WORequest+SOGo.m b/SoObjects/SOGo/WORequest+SOGo.m index 824cb8bc5..c1c0d042a 100644 --- a/SoObjects/SOGo/WORequest+SOGo.m +++ b/SoObjects/SOGo/WORequest+SOGo.m @@ -37,7 +37,7 @@ - (BOOL) handledByDefaultHandler { #warning this should be changed someday - return ![self isSoWebDAVRequest]; + return ![[self requestHandlerKey] isEqualToString:@"dav"]; } - (NSArray *) _propertiesOfElement: (id ) startElement diff --git a/Tests/Integration/README b/Tests/Integration/README index db62e4ed4..5cab950d4 100644 --- a/Tests/Integration/README +++ b/Tests/Integration/README @@ -52,5 +52,5 @@ AssertionError: event creation/modification: expected status code '403' (receive - Always set a doc string on the test methods, especially for complex test cases. -- When writing tests, be aware that contrary to unit tests, functional tests +- When writing tests, be aware that contrarily to unit tests, functional tests often imply a logical order between the different steps. diff --git a/Tests/Integration/all.py b/Tests/Integration/all.py index 608814467..525a1e7b5 100755 --- a/Tests/Integration/all.py +++ b/Tests/Integration/all.py @@ -12,11 +12,11 @@ if __name__ == "__main__": "Russian", "Spanish", "Swedish", "Welsh"] # We can disable testing all languages - testLanguages = True - opts, args = getopt.getopt (sys.argv[1:], [], ["disable-languages"]) + testLanguages = False + opts, args = getopt.getopt (sys.argv[1:], [], ["enable-languages"]) for o, a in opts: - if o == "--disable-languages": - testLanguages = False + if o == "--enable-languages": + testLanguages = True for mod in os.listdir("."): diff --git a/Tests/Integration/test-caldav-scheduling.py b/Tests/Integration/test-caldav-scheduling.py index 7538c32fd..1c3b189ef 100755 --- a/Tests/Integration/test-caldav-scheduling.py +++ b/Tests/Integration/test-caldav-scheduling.py @@ -13,29 +13,9 @@ import vobject import vobject.base import vobject.icalendar import webdavlib +import utilities import StringIO - -def fetchUserInfo(login): - client = webdavlib.WebDAVClient(hostname, port, username, password) - resource = "/SOGo/dav/%s/" % login - propfind = webdavlib.WebDAVPROPFIND(resource, - ["displayname", - "{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], - 0) - propfind.xpath_namespace = { "D": "DAV:", - "C": "urn:ietf:params:xml:ns:caldav" } - client.execute(propfind) - assert(propfind.response["status"] == 207) - name_nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/D:displayname', - None) - email_nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', - None) - if len(name_nodes[0].childNodes) > 0: - displayName = name_nodes[0].childNodes[0].nodeValue - else: - displayName = "" - - return (displayName, email_nodes[0].childNodes[0].nodeValue) +import xml.etree.ElementTree class CalDAVPropertiesTest(unittest.TestCase): def setUp(self): @@ -58,34 +38,29 @@ class CalDAVPropertiesTest(unittest.TestCase): ["{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp"], 0) self.client.execute(propfind) - propfind.xpath_namespace = { "D": "DAV:", - "C": "urn:ietf:params:xml:ns:caldav" } - propstats = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:schedule-calendar-transp') - self.assertTrue(len(propstats) > 0, - "schedule-calendar-transp not present in response") - node = propstats[0] - status = propfind.xpath_evaluate('D:status', - node.parentNode.parentNode)[0] \ - .childNodes[0].nodeValue[9:12] + response = propfind.response["document"].find('{DAV:}response') + propstat = response.find('{DAV:}propstat') + status = propstat.find('{DAV:}status').text[9:12] + self.assertEquals(status, "200", "schedule-calendar-transp marked as 'Not Found' in response") - values = node.childNodes - nvalues = len(values) - self.assertEquals(nvalues, 1, - "expected 1 value (%d received)" % nvalues) + transp = propstat.find('{DAV:}prop/{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp') + values = transp.getchildren() + self.assertEquals(len(values), 1, "one and only one element expected") value = values[0] - self.assertEquals(value.__class__.__name__, "Element", + self.assertTrue(isinstance(value, xml.etree.ElementTree._ElementInterface), "schedule-calendar-transp must be an instance of" \ " %s, not %s" - % ("Element", value.__class__.__name__)) - self.assertEquals(value.namespaceURI, "urn:ietf:params:xml:ns:caldav", - "schedule-calendar-transp must have a value in"\ - " namespace '%s', not '%s'" - % ("urn:ietf:params:xml:ns:caldav", - value.namespaceURI)) - self.assertTrue(value.tagName == "opaque", + % ("_ElementInterface", transp.__class__.__name__)) + ns = value.tag[0:31] + tag = value.tag[31:] + self.assertTrue(ns == "{urn:ietf:params:xml:ns:caldav}", + "schedule-calendar-transp must have a value in"\ + " namespace '%s', not '%s'" + % ("urn:ietf:params:xml:ns:caldav", ns)) + self.assertTrue(tag == "opaque", "schedule-calendar-transp must be 'opaque' on new" \ - " collections, not '%s'" % value.tagName) + " collections, not '%s'" % tag) ## PROPPATCH newValueNode = "{urn:ietf:params:xml:ns:caldav}thisvaluedoesnotexist" @@ -123,9 +98,10 @@ class CalDAVITIPDelegationTest(unittest.TestCase): def setUp(self): self.client = webdavlib.WebDAVClient(hostname, port, username, password) - (self.user_name, self.user_email) = fetchUserInfo(username) - (self.attendee1_name, self.attendee1_email) = fetchUserInfo(attendee1) - (self.attendee1_delegate_name, self.attendee1_delegate_email) = fetchUserInfo(attendee1_delegate) + utility = utilities.TestUtility(self, self.client) + (self.user_name, self.user_email) = utility.fetchUserInfo(username) + (self.attendee1_name, self.attendee1_email) = utility.fetchUserInfo(attendee1) + (self.attendee1_delegate_name, self.attendee1_delegate_email) = utility.fetchUserInfo(attendee1_delegate) self.user_calendar = "/SOGo/dav/%s/Calendar/personal/" % username self.attendee1_calendar = "/SOGo/dav/%s/Calendar/personal/" % attendee1 diff --git a/Tests/Integration/test-davacl.py b/Tests/Integration/test-davacl.py index 35e474f9e..359ba9e2d 100755 --- a/Tests/Integration/test-davacl.py +++ b/Tests/Integration/test-davacl.py @@ -102,7 +102,8 @@ class DAVCalendarAclTest(DAVAclTest): def __init__(self, arg): DAVAclTest.__init__(self, arg) - self.acl_utility = utilities.TestCalendarACLUtility(self.client, + self.acl_utility = utilities.TestCalendarACLUtility(self, + self.client, self.resource) def setUp(self): @@ -195,31 +196,21 @@ class DAVCalendarAclTest(DAVAclTest): " (received '%d')" % (filename, exp_status, delete.response["status"])) - def _currentUserPrivilegeSet(self, resource, expectFailure = False): + def _currentUserPrivilegeSet(self, resource, expStatus = 207): propfind = webdavlib.WebDAVPROPFIND(resource, ["{DAV:}current-user-privilege-set"], 0) self.subscriber_client.execute(propfind) - if expectFailure: - expStatus = 403 - else: - expStatus = 207 self.assertEquals(propfind.response["status"], expStatus, "unexected status code when reading privileges:" + " %s instead of %d" % (propfind.response["status"], expStatus)) privileges = [] - if not expectFailure: - propfind.xpath_namespace = { "D": "DAV:" } - response_nodes = propfind.xpath_evaluate("/D:multistatus/D:response/D:propstat/D:prop/D:current-user-privilege-set/D:privilege") + if expStatus < 300: + response_nodes = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}current-user-privilege-set/{DAV:}privilege") for node in response_nodes: - privilegeNode = node.childNodes[0] - tagName = privilegeNode.tagName - indexColon = tagName.find(":") - if indexColon > -1: - tagName = tagName[indexColon+1:] - privileges.append("{%s}%s" % (privilegeNode.namespaceURI, tagName)) + privileges.extend([x.tag for x in node.getchildren()]) return privileges @@ -259,22 +250,24 @@ class DAVCalendarAclTest(DAVAclTest): '{urn:ietf:params:xml:ns:caldav}schedule-respond-vtodo'] expectedPrivileges.extend(extraPrivileges) if rights.has_key("d"): - extraPrivileges = ["{DAV:}unbind"] - expectedPrivileges.extend(extraPrivileges) - privileges = self._currentUserPrivilegeSet(self.resource, - len(expectedPrivileges) == 0) + expectedPrivileges.append("{DAV:}unbind") + if len(expectedPrivileges) == 0: + expStatus = 404 + else: + expStatus = 207 + privileges = self._currentUserPrivilegeSet(self.resource, expStatus) self._comparePrivilegeSets(expectedPrivileges, privileges) - def _testEventDAVAcl(self, event_class, right): + def _testEventDAVAcl(self, event_class, right, error_code): icsClass = self.classToICSClass[event_class].lower() for suffix in [ "event", "task" ]: url = "%s%s-%s.ics" % (self.resource, icsClass, suffix) if right is None: - expectFailure = True + expStatus = error_code expectedPrivileges = None else: - expectFailure = False + expStatus = 207 expectedPrivileges = ['{DAV:}read-current-user-privilege-set', '{urn:inverse:params:xml:ns:inverse-dav}view-date-and-time', '{DAV:}read'] @@ -290,8 +283,8 @@ class DAVCalendarAclTest(DAVAclTest): '{DAV:}write'] expectedPrivileges.extend(extraPrivileges) - privileges = self._currentUserPrivilegeSet(url, expectFailure) - if not expectFailure: + privileges = self._currentUserPrivilegeSet(url, expStatus) + if expStatus != error_code: self._comparePrivilegeSets(expectedPrivileges, privileges) def _testRights(self, rights): @@ -306,6 +299,8 @@ class DAVCalendarAclTest(DAVAclTest): def _testCreate(self, rights): if rights.has_key("c") and rights["c"]: exp_code = 201 + elif len(rights) == 0: + exp_code = 404 else: exp_code = 403 self._putEvent(self.subscriber_client, "creation-test.ics", "PUBLIC", @@ -314,6 +309,8 @@ class DAVCalendarAclTest(DAVAclTest): def _testDelete(self, rights): if rights.has_key("d") and rights["d"]: exp_code = 204 + elif len(rights) == 0: + exp_code = 404 else: exp_code = 403 self._deleteEvent(self.subscriber_client, "public-event.ics", @@ -338,9 +335,13 @@ class DAVCalendarAclTest(DAVAclTest): event = self._webdavSyncEvent(event_class) self._checkViewEventRight("webdav-sync", event, event_class, right) - self._testModify(event_class, right) - self._testRespondTo(event_class, right) - self._testEventDAVAcl(event_class, right) + if len(rights) > 0: + error_code = 403 + else: + error_code = 404 + self._testModify(event_class, right, error_code) + self._testRespondTo(event_class, right, error_code) + self._testEventDAVAcl(event_class, right, error_code) def _getEvent(self, event_class, is_invitation = False): icsClass = self.classToICSClass[event_class] @@ -373,32 +374,27 @@ class DAVCalendarAclTest(DAVAclTest): return task def _calendarDataInMultistatus(self, query, filename, - response_tag = "D:response"): + response_tag = "{DAV:}response"): event = None - query.xpath_namespace = { "D": "DAV:", - "C": "urn:ietf:params:xml:ns:caldav" } - response_nodes = query.xpath_evaluate("/D:multistatus/%s" % response_tag) + # print "\n\n\n%s\n\n" % query.response["body"] + # print "\n\n" + response_nodes = query.response["document"].findall("%s" % response_tag) for response_node in response_nodes: - href_node = query.xpath_evaluate("D:href", response_node)[0] - href = href_node.childNodes[0].nodeValue + href_node = response_node.find("{DAV:}href") + href = href_node.text if href.endswith(filename): - propstat_nodes = query.xpath_evaluate("D:propstat", response_node) - for propstat_node in propstat_nodes: - status_node = query.xpath_evaluate("D:status", - propstat_node)[0] - status = status_node.childNodes[0].nodeValue - data_nodes = query.xpath_evaluate("D:prop/C:calendar-data", - propstat_node) + 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"): - if (len(data_nodes) > 0 - and len(data_nodes[0].childNodes) > 0): - event = data_nodes[0].childNodes[0].nodeValue - else: - if not (status.endswith("404 Resource Not Found") - or status.endswith("404 Not Found")): - self.fail("%s: unexpected status code: '%s'" - % (filename, status)) + 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'" + % (filename, status)) return event @@ -411,11 +407,11 @@ class DAVCalendarAclTest(DAVAclTest): ["{urn:ietf:params:xml:ns:caldav}calendar-data"], 1) self.subscriber_client.execute(propfind) - if propfind.response["status"] != 403: + if propfind.response["status"] != 404: event = self._calendarDataInMultistatus(propfind, filename) return event - + def _multigetEvent(self, event_class): event = None @@ -425,7 +421,7 @@ class DAVCalendarAclTest(DAVAclTest): ["{urn:ietf:params:xml:ns:caldav}calendar-data"], [ url ]) self.subscriber_client.execute(multiget) - if multiget.response["status"] != 403: + if multiget.response["status"] != 404: event = self._calendarDataInMultistatus(multiget, url) return event @@ -438,9 +434,9 @@ class DAVCalendarAclTest(DAVAclTest): sync_query = webdavlib.WebDAVSyncQuery(self.resource, None, ["{urn:ietf:params:xml:ns:caldav}calendar-data"]) self.subscriber_client.execute(sync_query) - if sync_query.response["status"] != 403: + if sync_query.response["status"] != 404: event = self._calendarDataInMultistatus(sync_query, url, - "D:sync-response") + "{DAV:}sync-response") return event @@ -497,17 +493,17 @@ class DAVCalendarAclTest(DAVAclTest): "expected key '%s' not found in secure event" % key) - def _testModify(self, event_class, right): + def _testModify(self, event_class, right, error_code): if right == "m" or right == "r": exp_code = 204 else: - exp_code = 403 + exp_code = error_code icsClass = self.classToICSClass[event_class] filename = "%s-event.ics" % icsClass.lower() self._putEvent(self.subscriber_client, filename, icsClass, exp_code) - def _testRespondTo(self, event_class, right): + def _testRespondTo(self, event_class, right, error_code): icsClass = self.classToICSClass[event_class] filename = "invitation-%s-event.ics" % icsClass.lower() self._putEvent(self.client, filename, icsClass, @@ -518,7 +514,7 @@ class DAVCalendarAclTest(DAVAclTest): if right == "m" or right == "r": exp_code = 204 else: - exp_code = 403 + exp_code = error_code # here we only do 'passive' validation: if a user has a "respond to" # right, only the attendee entry will me modified. The change of @@ -660,7 +656,8 @@ END:VCARD""" } def __init__(self, arg): DAVAclTest.__init__(self, arg) - self.acl_utility = utilities.TestAddressBookACLUtility(self.client, + self.acl_utility = utilities.TestAddressBookACLUtility(self, + self.client, self.resource) def setUp(self): @@ -685,28 +682,23 @@ END:VCARD""" } def testCreateDelete(self): """'create', 'delete'""" - self._testRights({ "c": True, - "d": True }) + self._testRights({ "c": True, "d": True }) def testViewCreate(self): """'view' and 'create'""" - self._testRights({ "c": True, - "v": True }) + self._testRights({ "c": True, "v": True }) def testViewDelete(self): """'view' and 'delete'""" - self._testRights({ "d": True, - "v": True }) + self._testRights({ "d": True, "v": True }) def testEditCreate(self): """'edit' and 'create'""" - self._testRights({ "c": True, - "e": True }) + self._testRights({ "c": True, "e": True }) def testEditDelete(self): """'edit' and 'delete'""" - self._testRights({ "d": True, - "e": True }) + self._testRights({ "d": True, "e": True }) def _testRights(self, rights): self.acl_utility.setupRights(subscriber_username, rights) @@ -775,6 +767,178 @@ END:VCARD""" } exp_code = 403 self._deleteCard(self.subscriber_client, "old.vcf", exp_code) +class DAVPublicAccessTest(unittest.TestCase): + def __init__(self, arg): + unittest.TestCase.__init__(self, arg) + self.client = webdavlib.WebDAVClient(hostname, port) + self.anon_client = webdavlib.WebDAVClient(hostname, port) + self.dav_utility = utilities.TestUtility(self, self.client) + + def testPublicAccess(self): + resource = '/SOGo/so/public' + options = webdavlib.HTTPOPTIONS(resource) + self.anon_client.execute(options) + self.assertEquals(options.response["status"], 404, + "/SOGo/so/public is unexpectedly available") + + resource = '/SOGo/public' + options = webdavlib.HTTPOPTIONS(resource) + self.anon_client.execute(options) + self.assertEquals(options.response["status"], 404, + "/SOGo/so/public is unexpectedly available") + + resource = '/SOGo/dav/%s' % username + options = webdavlib.HTTPOPTIONS(resource) + self.anon_client.execute(options) + self.assertEquals(options.response["status"], 401, + "Non-public resources should request authentication") + + resource = '/SOGo/dav/public' + options = webdavlib.HTTPOPTIONS(resource) + self.anon_client.execute(options) + self.assertNotEquals(options.response["status"], 401, + "Non-public resources must NOT request authentication") + self.assertEquals(options.response["status"], 200, + "/SOGo/dav/public is not available, check user defaults") + + +class DAVCalendarPublicAclTest(unittest.TestCase): + def setUp(self): + self.createdRsrc = None + self.client = webdavlib.WebDAVClient(hostname, port, + username, password) + self.subscriber_client = webdavlib.WebDAVClient(hostname, port, + subscriber_username, + subscriber_password) + self.anon_client = webdavlib.WebDAVClient(hostname, port) + + def tearDown(self): + if self.createdRsrc is not None: + delete = webdavlib.WebDAVDELETE(self.createdRsrc) + self.client.execute(delete) + + def testCollectionAccessNormalUser(self): + """normal user access to (non-)shared resource from su""" + + # 1. all rights removed + parentColl = '/SOGo/dav/%s/Calendar/' % username + self.createdRsrc = '%stest-dav-acl/' % parentColl + for rsrc in [ 'personal', 'test-dav-acl' ]: + resource = '%s%s/' % (parentColl, rsrc) + mkcol = webdavlib.WebDAVMKCOL(resource) + self.client.execute(mkcol) + acl_utility = utilities.TestCalendarACLUtility(self, + self.client, + resource) + acl_utility.setupRights("anonymous", {}) + acl_utility.setupRights(subscriber_username, {}) + acl_utility.setupRights("", {}) + + propfind = webdavlib.WebDAVPROPFIND(parentColl, [ "displayname" ], 1) + self.subscriber_client.execute(propfind) + hrefs = propfind.response["document"] \ + .findall("{DAV:}response/{DAV:}href") + self.assertEquals(len(hrefs), 1, + "expected only one href in response") + self.assertEquals(hrefs[0].text, parentColl, + "the href must be the 'Calendar' parent coll.") + + + acl_utility = utilities.TestCalendarACLUtility(self, + self.client, + self.createdRsrc) + + # 2. creation right added + 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") + 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.") + + acl_utility.setupRights(subscriber_username, {}) + + # 3. creation right added for "default user" + # subscriber_username expected to have access, but not "anonymous" + acl_utility.setupRights("", { "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: %d received" \ + % len(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.") + + anonParentColl = '/SOGo/dav/public/%s/Calendar/' % username + anon_propfind = webdavlib.WebDAVPROPFIND(anonParentColl, + [ "displayname" ], 1) + + self.anon_client.execute(anon_propfind) + hrefs = anon_propfind.response["document"] \ + .findall("{DAV:}response/{DAV:}href") + self.assertEquals(len(hrefs), 1, "expected only 1 href in response") + self.assertEquals(hrefs[0].text, anonParentColl, + "the first href is not a 'Calendar' parent coll.") + + acl_utility.setupRights("", {}) + + # 4. creation right added for "anonymous" + # "anonymous" expected to have access, but not subscriber_username + acl_utility.setupRights("anonymous", { "c": True }) + + 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(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)) + + self.subscriber_client.execute(propfind) + hrefs = propfind.response["document"] \ + .findall("{DAV:}response/{DAV:}href") + self.assertEquals(len(hrefs), 1, "expected only 1 href in response") + self.assertEquals(hrefs[0].text, parentColl, + "the first href is not a 'Calendar' parent coll.") + + def testCollectionAccessSuperUser(self): + # super user accessing (non-)shared res from nu + + parentColl = '/SOGo/dav/%s/Calendar/' % subscriber_username + self.createdRsrc = '%stest-dav-acl/' % parentColl + for rsrc in [ 'personal', 'test-dav-acl' ]: + resource = '%s%s/' % (parentColl, rsrc) + mkcol = webdavlib.WebDAVMKCOL(resource) + self.client.execute(mkcol) + acl_utility = utilities.TestCalendarACLUtility(self, + self.subscriber_client, + resource) + acl_utility.setupRights(username, {}) + + propfind = webdavlib.WebDAVPROPFIND(parentColl, [ "displayname" ], 1) + self.subscriber_client.execute(propfind) + hrefs = [x.text \ + for x in propfind.response["document"] \ + .findall("{DAV:}response/{DAV:}href")] + self.assertTrue(len(hrefs) > 2, + "expected at least 3 hrefs in response") + self.assertEquals(hrefs[0], parentColl, + "the href must be the 'Calendar' parent coll.") + for rsrc in [ 'personal', 'test-dav-acl' ]: + resource = '%s%s/' % (parentColl, rsrc) + self.assertTrue(hrefs.index(resource) > -1, + "resource '%s' not returned" % resource) + if __name__ == "__main__": unittest.main() - diff --git a/Tests/Integration/test-ical.py b/Tests/Integration/test-ical.py index fce16ae2b..51831fb86 100755 --- a/Tests/Integration/test-ical.py +++ b/Tests/Integration/test-ical.py @@ -16,7 +16,6 @@ class iCalTest(unittest.TestCase): propfind = webdavlib.WebDAVPROPFIND(resource, ["{DAV:}principal-collection-set"], 0) - propfind.xpath_namespace = { "D": "DAV:" } client.execute(propfind) self.assertEquals(propfind.response["status"], 207) headers = propfind.response["headers"] @@ -28,7 +27,6 @@ class iCalTest(unittest.TestCase): ["{DAV:}principal-collection-set"], 0) client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - propfind.xpath_namespace = { "D": "DAV:" } client.execute(propfind) self.assertEquals(propfind.response["status"], 207) headers = propfind.response["headers"] @@ -50,7 +48,6 @@ class iCalTest(unittest.TestCase): proppatch = webdavlib.WebDAVPROPPATCH(resource, props) client = webdavlib.WebDAVClient(hostname, port, username, password) client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - proppatch.xpath_namespace = { "D": "DAV:" } client.execute(proppatch) self.assertEquals(proppatch.response["status"], 207, "failure (%s) setting '%s' permission for '%s' on %s's calendars" @@ -63,11 +60,10 @@ class iCalTest(unittest.TestCase): ["{DAV:}group-membership"], 0) client = webdavlib.WebDAVClient(hostname, port, username, password) client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - propfind.xpath_namespace = { "D": "DAV:" } client.execute(propfind) - hrefs = propfind.xpath_evaluate("/D:multistatus/D:response/D:propstat/D:prop/D:group-membership/D:href") - members = [x.childNodes[0].nodeValue for x in hrefs] + hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}group-membership/{DAV:}href") + members = [x.text for x in hrefs] return members @@ -77,12 +73,11 @@ class iCalTest(unittest.TestCase): propfind = webdavlib.WebDAVPROPFIND(resource, [prop], 0) client = webdavlib.WebDAVClient(hostname, port, username, password) client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" - propfind.xpath_namespace = { "D": "DAV:", "n1": "http://calendarserver.org/ns/" } client.execute(propfind) - hrefs = propfind.xpath_evaluate("/D:multistatus/D:response/D:propstat/D:prop/n1:calendar-proxy-%s-for/D:href" + hrefs = propfind.response["document"].findall("{DAV:}response/{DAV:}propstat/{DAV:}prop/{http://calendarserver.org/ns/}calendar-proxy-%s-for/{DAV:}href" % perm) - members = [x.childNodes[0].nodeValue[len("/SOGo/dav/"):-1] for x in hrefs] + members = [x.text[len("/SOGo/dav/"):-1] for x in hrefs] return members @@ -123,7 +118,7 @@ class iCalTest(unittest.TestCase): % (users[1], perm, users[0], proxyFor)) def _testMapping(self, client, perm, resource, rights): - dav_utility = utilities.TestCalendarACLUtility(client, resource) + dav_utility = utilities.TestCalendarACLUtility(self, client, resource) dav_utility.setupRights(subscriber_username, rights) membership = self._getMembership(subscriber_username) @@ -142,7 +137,8 @@ class iCalTest(unittest.TestCase): client = webdavlib.WebDAVClient(hostname, port, username, password) client.user_agent = "DAVKit/4.0.1 (730); CalendarStore/4.0.1 (973); iCal/4.0.1 (1374); Mac OS X/10.6.2 (10C540)" personal_resource = "/SOGo/dav/%s/Calendar/personal/" % username - dav_utility = utilities.TestCalendarACLUtility(client, + dav_utility = utilities.TestCalendarACLUtility(self, + client, personal_resource) dav_utility.setupRights(subscriber_username, {}) dav_utility.subscribe([subscriber_username]) @@ -153,7 +149,8 @@ class iCalTest(unittest.TestCase): client.execute(delete) mkcol = webdavlib.WebDAVMKCOL(other_resource) client.execute(mkcol) - dav_utility = utilities.TestCalendarACLUtility(client, + dav_utility = utilities.TestCalendarACLUtility(self, + client, other_resource) dav_utility.setupRights(subscriber_username, {}) dav_utility.subscribe([subscriber_username]) @@ -181,7 +178,7 @@ class iCalTest(unittest.TestCase): ## we test the unsubscription # unsubscribed from personal, subscribed to 'test-calendar-proxy2' - dav_utility = utilities.TestCalendarACLUtility(client, + dav_utility = utilities.TestCalendarACLUtility(self, client, personal_resource) dav_utility.unsubscribe([subscriber_username]) membership = self._getMembership(subscriber_username) @@ -190,7 +187,7 @@ class iCalTest(unittest.TestCase): "'%s' must have write access to %s's calendars" % (subscriber_username, username)) # unsubscribed from personal, unsubscribed from 'test-calendar-proxy2' - dav_utility = utilities.TestCalendarACLUtility(client, + dav_utility = utilities.TestCalendarACLUtility(self, client, other_resource) dav_utility.unsubscribe([subscriber_username]) membership = self._getMembership(subscriber_username) diff --git a/Tests/Integration/test-maildav.py b/Tests/Integration/test-maildav.py index 950168333..c19137d6e 100755 --- a/Tests/Integration/test-maildav.py +++ b/Tests/Integration/test-maildav.py @@ -17,10 +17,8 @@ def fetchUserEmail(login): propfind = webdavlib.WebDAVPROPFIND(resource, ["{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], 0) - propfind.xpath_namespace = { "D": "DAV:", - "C": "urn:ietf:params:xml:ns:caldav" } client.execute(propfind) - nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', + nodes = propfind.xpath_evaluate('{DAV:}response/{DAV:}propstat/{DAV:}prop/C:calendar-user-address-set/{DAV:}href', None) return nodes[0].childNodes[0].nodeValue @@ -225,7 +223,7 @@ class DAVMailCollectionTest(): self.client.execute(propfind) key = property.replace("{urn:schemas:httpmail:}", "a:") key = key.replace("{urn:schemas:mailheader:}", "a:") - tmp = propfind.xpath_evaluate("/D:multistatus/D:response/D:propstat/D:prop") + tmp = propfind.xpath_evaluate("{DAV:}response/{DAV:}propstat/{DAV:}prop") prop = tmp[0].firstChild; result = None @@ -319,11 +317,9 @@ class DAVMailCollectionTest(): self.assertEquals(query.response["status"], 207, "filter %s:\n\tunexpected status: %d" % (filter[0], query.response["status"])) - query.xpath_namespace = { "D": "DAV:", - "I": "urn:inverse:params:xml:ns:inverse-dav" } - response_nodes = query.xpath_evaluate("/D:multistatus/D:response") + response_nodes = query.xpath_evaluate("{DAV:}response") for response_node in response_nodes: - href_node = query.xpath_evaluate("D:href", response_node)[0] + href_node = query.xpath_evaluate("{DAV:}href", response_node)[0] href = href_node.childNodes[0].nodeValue received_count = received_count + 1 self.assertTrue(expected_hrefs.has_key(href), @@ -345,12 +341,10 @@ class DAVMailCollectionTest(): self.assertEquals(query.response["status"], 207, "sortOrder %s:\n\tunexpected status: %d" % (sortOrder[0], query.response["status"])) - query.xpath_namespace = { "D": "DAV:", - "I": "urn:inverse:params:xml:ns:inverse-dav" } - response_nodes = query.xpath_evaluate("/D:multistatus/D:response") + response_nodes = query.response["document"].findall("{DAV:}response") for response_node in response_nodes: - href_node = query.xpath_evaluate("D:href", response_node)[0] - href = href_node.childNodes[0].nodeValue + href_node = response_node.find("{DAV:}href") + href = href_node.text self.assertEquals(expected_hrefs[received_count], href, "sortOrder %s:\n\tunexpected href: %s (expecting: %s)" % (sortOrder[0], href, diff --git a/Tests/Integration/test-webdav.py b/Tests/Integration/test-webdav.py index 2eefcbe32..375e3e9f4 100755 --- a/Tests/Integration/test-webdav.py +++ b/Tests/Integration/test-webdav.py @@ -11,7 +11,7 @@ class WebDAVTest(unittest.TestCase): unittest.TestCase.__init__(self, arg) self.client = webdavlib.WebDAVClient(hostname, port, username, password) - self.dav_utility = utilities.TestUtility(self.client) + self.dav_utility = utilities.TestUtility(self, self.client) def testPrincipalCollectionSet(self): """property: 'principal-collection-set' on collection object""" @@ -19,12 +19,11 @@ class WebDAVTest(unittest.TestCase): propfind = webdavlib.WebDAVPROPFIND(resource, ["{DAV:}principal-collection-set"], 0) - propfind.xpath_namespace = { "D": "DAV:" } self.client.execute(propfind) self.assertEquals(propfind.response["status"], 207) - nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/D:principal-collection-set/D:href', - None) - responseHref = nodes[0].childNodes[0].nodeValue + nodes = propfind.response["document"] \ + .findall('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}principal-collection-set/{DAV:}href') + responseHref = nodes[0].text if responseHref[0:4] == "http": self.assertEquals("http://%s/SOGo/dav/" % hostname, responseHref, "{DAV:}principal-collection-set returned %s instead of 'http../SOGo/dav/'" @@ -40,12 +39,11 @@ class WebDAVTest(unittest.TestCase): propfind = webdavlib.WebDAVPROPFIND(resource, ["{DAV:}principal-collection-set"], 0) - propfind.xpath_namespace = { "D": "DAV:" } self.client.execute(propfind) self.assertEquals(propfind.response["status"], 207) - nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/D:principal-collection-set/D:href', - None) - responseHref = nodes[0].childNodes[0].nodeValue + node = propfind.response["document"] \ + .find('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}principal-collection-set/{DAV:}href') + responseHref = node.text expectedHref = '/SOGo/dav/' if responseHref[0:4] == "http": self.assertEquals("http://%s%s" % (hostname, expectedHref), responseHref, @@ -61,19 +59,15 @@ class WebDAVTest(unittest.TestCase): propfind = webdavlib.WebDAVPROPFIND(resource, ["{DAV:}displayname", "{DAV:}resourcetype"], 1) - propfind.xpath_namespace = { "D": "DAV:" } self.client.execute(propfind) self.assertEquals(propfind.response["status"], 207) - nodes = propfind.xpath_evaluate('/D:multistatus/D:response', - None) + nodes = propfind.response["document"].findall('{DAV:}response') for node in nodes: - responseHref = propfind.xpath_evaluate('D:href', node)[0].childNodes[0].nodeValue + responseHref = node.find('{DAV:}href').text hasSlash = responseHref[-1] == '/' - resourcetypes = \ - propfind.xpath_evaluate('D:propstat/D:prop/D:resourcetype', - node)[0].childNodes - isCollection = len(resourcetypes) > 0 + resourcetype = node.find('{DAV:}propstat/{DAV:}prop/{DAV:}resourcetype') + isCollection = len(resourcetype.getchildren()) > 0 if isCollection: self.assertEquals(hasSlash, resourceWithSlash, "failure with href '%s' while querying '%s'" @@ -108,14 +102,11 @@ class WebDAVTest(unittest.TestCase): ["displayname"], matches) self.client.execute(query) self.assertEquals(query.response["status"], 207) - response = query.xpath_evaluate('/D:multistatus/D:response')[0] - href = query.xpath_evaluate('D:href', response)[0] - self.assertEquals("/SOGo/dav/%s/" % username, - href.childNodes[0].nodeValue) - displayname = query.xpath_evaluate('/D:multistatus/D:response' \ - + '/D:propstat/D:prop' \ - + '/D:displayname')[0] - value = displayname.nodeValue + response = query.response["document"].findall('{DAV:}response')[0] + href = response.find('{DAV:}href').text + self.assertEquals("/SOGo/dav/%s/" % username, href) + displayname = response.find('{DAV:}propstat/{DAV:}prop/{DAV:}displayname') + value = displayname.text if value is None: value = "" self.assertEquals(userInfo[0], value) @@ -126,60 +117,38 @@ class WebDAVTest(unittest.TestCase): resource = '/SOGo/dav/%s/' % username userInfo = self.dav_utility.fetchUserInfo(username) - query_props = {"owner": { "href": resource, - "displayname": userInfo[0]}, - "principal-collection-set": { "href": "/SOGo/dav/", - "displayname": "SOGo"}} + query_props = {"{DAV:}owner": { "{DAV:}href": resource, + "{DAV:}displayname": userInfo[0]}, + "{DAV:}principal-collection-set": { "{DAV:}href": "/SOGo/dav/", + "{DAV:}displayname": "SOGo"}} query = webdavlib.WebDAVExpandProperty(resource, query_props.keys(), ["displayname"]) self.client.execute(query) self.assertEquals(query.response["status"], 207) - topResponse = query.xpath_evaluate('/D:multistatus/D:response')[0] - topHref = query.xpath_evaluate('D:href', topResponse)[0] - self.assertEquals(resource, topHref.childNodes[0].nodeValue) + topResponse = query.response["document"].find('{DAV:}response') + topHref = topResponse.find('{DAV:}href') + self.assertEquals(resource, topHref.text) for query_prop in query_props.keys(): - propResponse = query.xpath_evaluate('D:propstat/D:prop/D:%s' - % query_prop, topResponse)[0] - - -# -# -# -# /SOGo/dav/wsourdeau/ -# -# -# -# -# /SOGo/dav/wsourdeau/ -# -# -# Wolfgang Sourdeau -# -# HTTP/1.1 200 OK -# -# -# - propHref = query.xpath_evaluate('D:response/D:href', - propResponse)[0] - self.assertEquals(query_props[query_prop]["href"], - propHref.childNodes[0].nodeValue, + propResponse = topResponse.find('{DAV:}propstat/{DAV:}prop/%s' + % query_prop) + propHref = propResponse.find('{DAV:}response/{DAV:}href') + self.assertEquals(query_props[query_prop]["{DAV:}href"], + propHref.text, "'%s', href mismatch: exp. '%s', got '%s'" % (query_prop, - query_props[query_prop]["href"], - propHref.childNodes[0].nodeValue)) - propDisplayname = query.xpath_evaluate('D:response/D:propstat/D:prop/D:displayname', - propResponse)[0] - if len(propDisplayname.childNodes) > 0: - displayName = propDisplayname.childNodes[0].nodeValue - else: + query_props[query_prop]["{DAV:}href"], + propHref.text)) + propDisplayname = propResponse.find('{DAV:}response/{DAV:}propstat/{DAV:}prop/{DAV:}displayname') + displayName = propDisplayname.text + if displayName is None: displayName = "" - self.assertEquals(query_props[query_prop]["displayname"], + self.assertEquals(query_props[query_prop]["{DAV:}displayname"], displayName, "'%s', displayname mismatch: exp. '%s', got '%s'" % (query_prop, - query_props[query_prop]["displayname"], - propDisplayname.nodeValue)) + query_props[query_prop]["{DAV:}displayname"], + propDisplayname)) if __name__ == "__main__": unittest.main() diff --git a/Tests/Integration/test-webdavsync.py b/Tests/Integration/test-webdavsync.py index 7159d28d9..4fb44bd83 100755 --- a/Tests/Integration/test-webdavsync.py +++ b/Tests/Integration/test-webdavsync.py @@ -43,10 +43,10 @@ class WebdavSyncTest(unittest.TestCase): self.assertEquals(query1.response["status"], 207, ("query1: invalid status code: %d (!= 207)" % query1.response["status"])) - token_node = query1.xpath_evaluate("/D:multistatus/D:sync-token")[0] + token_node = query1.response["document"].find("{DAV:}sync-token") # Implicit "assertion": we expect SOGo to return a token node, with a # non-empty numerical value. Anything else will trigger an exception - token = int(token_node.childNodes[0].nodeValue) + token = int(token_node.text) self.assertTrue(token > 0) self.assertTrue(token <= int(query1.start)) @@ -55,8 +55,8 @@ class WebdavSyncTest(unittest.TestCase): query2 = webdavlib.WebDAVSyncQuery(resource, "1234", [ "getetag" ]) self.client.execute(query2) self.assertEquals(query2.response["status"], 403) - cond_nodes = query2.xpath_evaluate("/D:error/D:valid-sync-token") - self.assertTrue(len(cond_nodes) > 0, + cond_nodes = query2.response["document"].find("{DAV:}valid-sync-token") + self.assertTrue(cond_nodes is not None, "expected 'valid-sync-token' condition error") if __name__ == "__main__": diff --git a/Tests/Integration/utilities.py b/Tests/Integration/utilities.py index feb08078a..352ad342d 100644 --- a/Tests/Integration/utilities.py +++ b/Tests/Integration/utilities.py @@ -2,9 +2,11 @@ import unittest import webdavlib +import xml.sax.saxutils -class TestUtility(unittest.TestCase): - def __init__(self, client): +class TestUtility(): + def __init__(self, test, client, resource = None): + self.test = test self.client = client self.userInfo = {} @@ -15,26 +17,26 @@ class TestUtility(unittest.TestCase): ["displayname", "{urn:ietf:params:xml:ns:caldav}calendar-user-address-set"], 0) - propfind.xpath_namespace = { "D": "DAV:", - "C": "urn:ietf:params:xml:ns:caldav" } self.client.execute(propfind) - assert(propfind.response["status"] == 207) - name_nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/D:displayname', - None) - email_nodes = propfind.xpath_evaluate('/D:multistatus/D:response/D:propstat/D:prop/C:calendar-user-address-set/D:href', - None) + self.test.assertEquals(propfind.response["status"], 207) + common_tree = "{DAV:}response/{DAV:}propstat/{DAV:}prop" + name_nodes = propfind.response["document"] \ + .findall('%s/{DAV:}displayname' % common_tree) + email_nodes = propfind.response["document"] \ + .findall('%s/{urn:ietf:params:xml:ns:caldav}calendar-user-address-set/{DAV:}href' + % common_tree) - if len(name_nodes[0].childNodes) > 0: - displayName = name_nodes[0].childNodes[0].nodeValue + if len(name_nodes[0].text) > 0: + displayName = name_nodes[0].text else: displayName = "" - self.userInfo[login] = (displayName, email_nodes[0].childNodes[0].nodeValue) + self.userInfo[login] = (displayName, email_nodes[0].text) return self.userInfo[login] class TestACLUtility(TestUtility): - def __init__(self, client, resource): - TestUtility.__init__(self, client) + def __init__(self, test, client, resource): + TestUtility.__init__(self, test, client, resource) self.resource = resource def _subscriptionOperation(self, subscribers, operation): @@ -48,10 +50,10 @@ class TestACLUtility(TestUtility): post = webdavlib.HTTPPOST(self.resource, subscribeQuery) post.content_type = "application/xml; charset=\"utf-8\"" self.client.execute(post) - self.assertEquals(post.response["status"], 204, - "subscribtion failure to '%s' for '%s' (status: %d)" - % (self.resource, "', '".join(subscribers), - post.response["status"])) + self.test.assertEquals(post.response["status"], 204, + "subscribtion failure to '%s' for '%s' (status: %d)" + % (self.resource, "', '".join(subscribers), + post.response["status"])) def subscribe(self, subscribers=None): self._subscriptionOperation(subscribers, "subscribe") @@ -68,16 +70,16 @@ class TestACLUtility(TestUtility): aclQuery = ("\n" + "" - + "%s" % (username, + + "%s" % (xml.sax.saxutils.escape(username), rights_str) + "") post = webdavlib.HTTPPOST(self.resource, aclQuery) post.content_type = "application/xml; charset=\"utf-8\"" self.client.execute(post) - self.assertEquals(post.response["status"], 204, - "rights modification: failure to set '%s' (status: %d)" - % (rights_str, post.response["status"])) + self.test.assertEquals(post.response["status"], 204, + "rights modification: failure to set '%s' (status: %d)" + % (rights_str, post.response["status"])) # Calendar: # rights: diff --git a/Tests/Integration/webdavlib.py b/Tests/Integration/webdavlib.py index cedd38bbe..eee5eb243 100644 --- a/Tests/Integration/webdavlib.py +++ b/Tests/Integration/webdavlib.py @@ -21,12 +21,11 @@ import cStringIO import httplib -import M2Crypto.httpslib import re import time +import xml.dom.expatbuilder +import xml.etree.ElementTree import xml.sax.saxutils -import xml.dom.ext.reader.Sax2 -import xml.xpath import sys xmlns_dav = "DAV:" @@ -66,19 +65,27 @@ class HTTPUnparsedURL: class WebDAVClient: user_agent = "Mozilla/5.0" - def __init__(self, hostname, port, username, password, forcessl = False): + def __init__(self, hostname, port, username = None, password = None, + forcessl = False): if int(port) == 443 or forcessl: + import M2Crypto.httpslib self.conn = M2Crypto.httpslib.HTTPSConnection(hostname, int(port), True) else: self.conn = httplib.HTTPConnection(hostname, port, True) - self.simpleauth_hash = (("%s:%s" % (username, password)) - .encode('base64')[:-1]) + if username is not None: + if password is None: + password = "" + self.simpleauth_hash = (("%s:%s" % (username, password)) + .encode('base64')[:-1]) + else: + self.simpleauth_hash = None def prepare_headers(self, query, body): - headers = { "User-Agent": self.user_agent, - "authorization": "Basic %s" % self.simpleauth_hash } + headers = { "User-Agent": self.user_agent } + if self.simpleauth_hash is not None: + headers["authorization"] = "Basic %s" % self.simpleauth_hash if body is not None: headers["content-length"] = len(body) if query.__dict__.has_key("depth") and query.depth is not None: @@ -143,13 +150,24 @@ class HTTPQuery(HTTPSimpleQuery): class HTTPPUT(HTTPQuery): method = "PUT" - def __init__(self, url, content): + def __init__(self, url, content, + content_type="application/octet-stream", + exclusive=False): HTTPQuery.__init__(self, url) self.content = content + self.content_type = content_type + self.exclusive = exclusive def render(self): return self.content + def prepare_headers(self): + headers = HTTPQuery.prepare_headers(self) + if self.exclusive: + headers["if-none-match"] = "*" + + return headers + class HTTPPOST(HTTPPUT): method = "POST" @@ -162,7 +180,6 @@ class WebDAVQuery(HTTPQuery): self.depth = depth self.ns_mgr = _WD_XMLNS_MGR() self.top_node = None - self.xpath_namespace = { "D": xmlns_dav } # helper for PROPFIND and REPORT (only) def _initProperties(self, properties): @@ -200,17 +217,9 @@ class WebDAVQuery(HTTPQuery): and (headers["content-type"].startswith("application/xml") or headers["content-type"].startswith("text/xml")) and int(headers["content-length"]) > 0): - reader = xml.dom.ext.reader.Sax2.Reader() + tree = xml.etree.ElementTree.ElementTree() stream = cStringIO.StringIO(self.response["body"]) - dom_response = reader.fromStream(stream) - self.response["document"] = dom_response.documentElement - - def xpath_evaluate(self, query, top_node = None): - if top_node is None: - top_node = self.response["document"] - xpath_context = xml.xpath.CreateContext(top_node) - xpath_context.setNamespaces(self.xpath_namespace) - return xml.xpath.Evaluate(query, None, xpath_context) + self.response["document"] = tree.parse(stream) class WebDAVMKCOL(WebDAVQuery): method = "MKCOL" @@ -262,20 +271,6 @@ class WebDAVMOVE(WebDAVQuery): headers["Host"] = self.host return headers -class WebDAVPUT(WebDAVQuery): - method = "PUT" - - def __init__(self, url, content): - WebDAVQuery.__init__(self, url) - self.content_type = "text/plain; charset=utf-8" - self.content = content - - def prepare_headers(self): - return WebDAVQuery.prepare_headers(self) - - def render(self): - return self.content - class WebDAVPrincipalPropertySearch(WebDAVREPORT): def __init__(self, url, properties, matches): WebDAVQuery.__init__(self, url) diff --git a/UI/MainUI/product.plist b/UI/MainUI/product.plist index 578b3e109..7bb31f229 100644 --- a/UI/MainUI/product.plist +++ b/UI/MainUI/product.plist @@ -11,7 +11,7 @@ superclass = "SoComponent"; protectedBy = ""; defaultRoles = { - "View" = ( "Authenticated" ); + "View" = ( "Authenticated", "PublicUser" ); }; }; SOGoObject = { @@ -67,10 +67,10 @@ }; SOGoParentFolder = { superclass = "SOGoFolder"; - protectedBy = "Access Contents Information"; + protectedBy = ""; defaultRoles = { - "Access Contents Information" = ( "Authenticated" ); - "WebDAV Access" = ( "Authenticated" ); + "Access Contents Information" = ( "Authenticated", "PublicUser" ); + "WebDAV Access" = ( "Authenticated", "PublicUser" ); "Add Folders" = ( "Owner" ); }; }; @@ -78,14 +78,23 @@ superclass = "SOGoFolder"; protectedBy = "Access Contents Information"; defaultRoles = { - "Access Contents Information" = ( "Authenticated" ); - "WebDAV Access" = ( "Authenticated" ); + "Access Contents Information" = ( "Authenticated", "PublicUser" ); + "WebDAV Access" = ( "Authenticated", "PublicUser" ); "View" = ( "Authenticated" ); }; }; SOGoGCSFolder = { superclass = "SOGoFolder"; }; + SOGoPublicBaseFolder = { + superclass = "SOGoFolder"; + protectedBy = "Access Contents Information"; + defaultRoles = { + "Access Contents Information" = ( "Authenticated", "PublicUser" ); + "WebDAV Access" = ( "Authenticated", "PublicUser" ); + "View" = ( "Authenticated" ); + }; + }; }; categories = {