From 126651b6a75325e3e61e963e3448fade1df75f06 Mon Sep 17 00:00:00 2001 From: Wolfgang Sourdeau Date: Thu, 28 Jan 2010 21:42:03 +0000 Subject: [PATCH] Monotone-Parent: 775b7e4fea80568033b5c8bf9b7c5220c7d36041 Monotone-Revision: ff12d30f9bf1800bb0b1bcbefba9493cdeeeccaf Monotone-Author: wsourdeau@inverse.ca Monotone-Date: 2010-01-28T21:42:03 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 46 +++ SoObjects/Mailer/SOGoMailAccount.m | 16 +- SoObjects/Mailer/SOGoMailBaseObject.h | 2 +- SoObjects/Mailer/SOGoMailBaseObject.m | 50 ++- SoObjects/Mailer/SOGoMailObject.m | 31 +- SoObjects/SOGo/GNUmakefile | 4 +- SoObjects/SOGo/SOGoAuthenticator.h | 3 + SoObjects/SOGo/SOGoCASSession.h | 66 ++++ SoObjects/SOGo/SOGoCASSession.m | 454 ++++++++++++++++++++++++++ SoObjects/SOGo/SOGoCache.h | 16 + SoObjects/SOGo/SOGoCache.m | 186 ++++++++--- SoObjects/SOGo/SOGoDAVAuthenticator.m | 7 + SoObjects/SOGo/SOGoObject.m | 2 +- SoObjects/SOGo/SOGoSystemDefaults.h | 4 + SoObjects/SOGo/SOGoSystemDefaults.m | 10 + SoObjects/SOGo/SOGoWebAuthenticator.m | 63 +++- UI/Common/UIxPageFrame.m | 7 +- UI/MainUI/SOGoRootPage.m | 157 +++++++-- UI/MainUI/SOGoUserHomePage.m | 7 +- UI/MainUI/product.plist | 5 + 20 files changed, 1034 insertions(+), 102 deletions(-) create mode 100644 SoObjects/SOGo/SOGoCASSession.h create mode 100644 SoObjects/SOGo/SOGoCASSession.m diff --git a/ChangeLog b/ChangeLog index f592d101a..2320907e2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,49 @@ +2010-01-28 Wolfgang Sourdeau + + * SoObjects/SOGo/SOGoSystemDefaults.m (-CASServiceURL) + (-authenticationType): new accessors. + + * SoObjects/SOGo/SOGoObject.m (+globallyUniqueObjectId): fixed to + use the result of "random" rather than the function pointer. + + * SoObjects/SOGo/SOGoCache.m (-setValue:forKey:expire) + (-setValue:forKey:, -valueForKey:, -removeValueForKey:): new + accessors. + (-CASTicketFromIdentifier:, -CASSessionWithTicket:) + (-setCASSession:withTicket:forIdentifier:): new accessors enabling + the CAS session management. + (-CASPGTIdFromPGTIOU:, -setCASPGTId:forPGTIOU:): new accessors + enabling proxy ticket management (see casProxyAction below). + + * UI/MainUI/SOGoUserHomePage.m (-logoffAction): we now pass the + request application name as cookie path. + + * UI/MainUI/SOGoRootPage.m (-casProxyAction): new method invoked + by the CAS server when authenticating the SOGo server during a + proxy request. + (-_casDefaultAction): new "defaultAction" method executed in CAS mode. + + * UI/Common/UIxPageFrame.m (-canLogoff): return NO in CAS mode. + + * SoObjects/SOGo/SOGoWebAuthenticator.m (-checkLogin:password:) + when authentication type is set to "cas", the password is the + ticket identifying the CAS session. We therefore check if the + login and the session login match. + (-imapPasswordInContext:forServer:forceRenew:): new method that + returns the "password" to use for IMAP connections. In CAS mode, + a proxy ticket is fetched with the current CAS session. In + standard mode, we still use the current user's password. The + "forceRenew:" parameter enables the fetching of a new proxy ticket + if the current one has expired. + (-setupAuthFailResponse:withReason:inContext:): we know invoke + "defaultAction" on the SOGoRootPage instance to make sure all the + cookies and CAS tickets are taken into account. + We now pass the request application name as cookie path. + + * SoObjects/SOGo/SOGoCASSession.[hm]: new class module + implementing the class in charge of CAS authentication and proxy + transactions. + 2010-01-27 Wolfgang Sourdeau * Tools/SOGoToolBackup.m (-fetchUserIDs): retain allUsers to avoid diff --git a/SoObjects/Mailer/SOGoMailAccount.m b/SoObjects/Mailer/SOGoMailAccount.m index d20b4adbb..4b6096738 100644 --- a/SoObjects/Mailer/SOGoMailAccount.m +++ b/SoObjects/Mailer/SOGoMailAccount.m @@ -225,7 +225,7 @@ static NSString *sieveScriptName = @"sogo"; SOGoUserDefaults *ud; SOGoDomainDefaults *dd; NGSieveClient *client; - NSString *v; + NSString *v, *password; BOOL b; dd = [[context activeUser] domainDefaults]; @@ -319,11 +319,21 @@ static NSString *sieveScriptName = @"sogo"; return NO; } - result = [client login: [[self imap4URL] user] password:[self imap4Password]]; + password = [self imap4PasswordRenewed: NO]; + if (!password) { + [client closeConnection]; + return NO; + } + result = [client login: [[self imap4URL] user] password: password]; + if (![[result valueForKey:@"result"] boolValue]) { + [self errorWithFormat: @"failure. Attempting with a renewed password."]; + password = [self imap4PasswordRenewed: NO]; + result = [client login: [[self imap4URL] user] password: password]; + } if (![[result valueForKey:@"result"] boolValue]) { [self errorWithFormat: @"Could not login '%@' (%@) on Sieve server: %@: %@", - [[self imap4URL] user], [self imap4Password], client, result]; + [[self imap4URL] user], password, client, result]; [client closeConnection]; return NO; } diff --git a/SoObjects/Mailer/SOGoMailBaseObject.h b/SoObjects/Mailer/SOGoMailBaseObject.h index 8294cbd07..11c56105c 100644 --- a/SoObjects/Mailer/SOGoMailBaseObject.h +++ b/SoObjects/Mailer/SOGoMailBaseObject.h @@ -70,7 +70,7 @@ - (NSURL *) imap4URL; - (NSString *) imap4Login; -- (NSString *) imap4Password; +- (NSString *) imap4PasswordRenewed: (BOOL) renew; - (void) flushMailCaches; diff --git a/SoObjects/Mailer/SOGoMailBaseObject.m b/SoObjects/Mailer/SOGoMailBaseObject.m index de3ab5f8b..eb77fa2f9 100644 --- a/SoObjects/Mailer/SOGoMailBaseObject.m +++ b/SoObjects/Mailer/SOGoMailBaseObject.m @@ -30,7 +30,7 @@ #import #import -#import +#import #import "SOGoMailManager.h" @@ -119,17 +119,36 @@ static BOOL debugOn = YES; - (NGImap4Connection *) imap4Connection { + NGImap4ConnectionManager *manager; + NSString *password; + if (!imap4) { - imap4 = [[self mailManager] connectionForURL: [self imap4URL] - password: [self imap4Password]]; - if (imap4) - [imap4 retain]; - else - [self errorWithFormat:@"Could not connect IMAP4."]; + [self imap4URL]; + manager = [self mailManager]; + password = [self imap4PasswordRenewed: NO]; + if (password) + { + imap4 = [manager connectionForURL: imap4URL + password: password]; + if (!imap4) + { + [self logWithFormat: @"renewing imap4 password"]; + password = [self imap4PasswordRenewed: YES]; + if (password) + imap4 = [manager connectionForURL: imap4URL + password: password]; + } + } + if (!imap4) + { + imap4 = (NGImap4Connection *) [NSNull null]; + [self errorWithFormat:@"Could not connect IMAP4"]; + } + [imap4 retain]; } - return imap4; + return [imap4 isKindOfClass: [NSNull class]] ? nil : imap4; } - (NSString *) relativeImap4Name @@ -184,7 +203,7 @@ static BOOL debugOn = YES; return [container imap4Login]; } -- (NSString *) imap4Password +- (NSString *) imap4PasswordRenewed: (BOOL) renewed { /* Extract password from basic authentication. @@ -193,8 +212,19 @@ static BOOL debugOn = YES; a) move the primary code to SOGoMailAccount b) cache the password */ + NSURL *imapURL; + NSString *password; - return [[self authenticatorInContext: context] passwordInContext: context]; + imapURL = [[self mailAccountFolder] imap4URL]; + + password = [[self authenticatorInContext: context] + imapPasswordInContext: context + forServer: [imapURL host] + forceRenew: renewed]; + if (!password) + [self errorWithFormat: @"no IMAP4 password available"]; + + return password; } - (NSMutableString *) traversalFromMailAccount diff --git a/SoObjects/Mailer/SOGoMailObject.m b/SoObjects/Mailer/SOGoMailObject.m index 2c0bf2ac5..5c291818b 100644 --- a/SoObjects/Mailer/SOGoMailObject.m +++ b/SoObjects/Mailer/SOGoMailObject.m @@ -931,14 +931,35 @@ static BOOL debugSoParts = NO; doesn't tell us the new ID. */ NSURL *destImap4URL; - + NGImap4ConnectionManager *manager; + NSException *exc; + NSString *password; + destImap4URL = ([_name length] == 0) ? [[_target container] imap4URL] : [_target imap4URL]; - - return [[self mailManager] copyMailURL:[self imap4URL] - toFolderURL:destImap4URL - password:[self imap4Password]]; + + manager = [self mailManager]; + [self imap4URL]; + password = [self imap4PasswordRenewed: NO]; + if (password) + { + exc = [manager copyMailURL: imap4URL + toFolderURL: destImap4URL + password: password]; + if (exc) + { + [self + logWithFormat: @"failure. Attempting with renewed imap4 password"]; + password = [self imap4PasswordRenewed: YES]; + if (password) + exc = [manager copyMailURL: imap4URL + toFolderURL: destImap4URL + password: password]; + } + } + + return exc; } /* actions */ diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index a54b1e664..bd8261152 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -46,6 +46,7 @@ SOGo_HEADER_FILES = \ NSURL+DAV.h \ \ SOGoAuthenticator.h \ + SOGoCASSession.h \ SOGoDAVAuthenticator.h \ SOGoProxyAuthenticator.h \ SOGoWebAuthenticator.h \ @@ -105,8 +106,9 @@ SOGo_OBJC_FILES = \ NSString+Utilities.m \ NSURL+DAV.m \ \ + SOGoCASSession.m \ SOGoDAVAuthenticator.m \ - SOGoProxyAuthenticator.m \ + SOGoProxyAuthenticator.m \ SOGoWebAuthenticator.m \ SOGoWebDAVAclManager.m \ SOGoWebDAVValue.m \ diff --git a/SoObjects/SOGo/SOGoAuthenticator.h b/SoObjects/SOGo/SOGoAuthenticator.h index 064edf5c4..4272036e0 100644 --- a/SoObjects/SOGo/SOGoAuthenticator.h +++ b/SoObjects/SOGo/SOGoAuthenticator.h @@ -33,6 +33,9 @@ - (NSString *) passwordInContext: (WOContext *) context; - (SOGoUser *) userInContext: (WOContext *) context; +- (NSString *) imapPasswordInContext: (WOContext *) context + forServer: (NSString *) imapServer + forceRenew: (BOOL) renew; @end diff --git a/SoObjects/SOGo/SOGoCASSession.h b/SoObjects/SOGo/SOGoCASSession.h new file mode 100644 index 000000000..ed16029c2 --- /dev/null +++ b/SoObjects/SOGo/SOGoCASSession.h @@ -0,0 +1,66 @@ +/* SOGoCASSession.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 SOGOCASSESSION_H +#define SOGOCASSESSION_H + +/* implementation of the CAS protocol as required for a client/proxy: + http://www.jasig.org/cas/protocol */ + +#import + +@class NSString; +@class NSMutableDictionary; + +@interface SOGoCASSession : NSObject +{ + NSString *ticket; + NSString *login; + NSString *pgt; + NSString *identifier; + NSMutableDictionary *proxyTickets; + BOOL cacheUpdateNeeded; + NSString *currentProxyService; +} + ++ (NSString *) CASURLWithAction: (NSString *) casAction + andParameters: (NSDictionary *) parameters; + ++ (SOGoCASSession *) CASSessionWithTicket: (NSString *) newTicket; ++ (SOGoCASSession *) CASSessionWithIdentifier: (NSString *) identifier; + +- (NSString *) identifier; + +- (void) setTicket: (NSString *) newTicket; +- (NSString *) ticket; + +- (NSString *) login; +- (NSString *) identifier; + +- (NSString *) ticketForService: (NSString *) service; +- (void) invalidateTicketForService: (NSString *) service; + +- (void) updateCache; + +@end + +#endif /* SOGOCASSESSION_H */ diff --git a/SoObjects/SOGo/SOGoCASSession.m b/SoObjects/SOGo/SOGoCASSession.m new file mode 100644 index 000000000..34b056ac4 --- /dev/null +++ b/SoObjects/SOGo/SOGoCASSession.m @@ -0,0 +1,454 @@ +/* SOGoCASSession.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 + +#import +#import +#import +#import + +#import +#import +#import +#import +#import +#import + +#import "NSDictionary+BSJSONAdditions.h" +#import "NSString+Utilities.h" +#import "SOGoCache.h" +#import "SOGoObject.h" +#import "SOGoSystemDefaults.h" + +#import "SOGoCASSession.h" + +@implementation SOGoCASSession + ++ (NSString *) CASURLWithAction: (NSString *) casAction + andParameters: (NSDictionary *) parameters +{ + NSString *casActionURL, *baseCASURL; + SOGoSystemDefaults *sd; + + sd = [SOGoSystemDefaults sharedSystemDefaults]; + baseCASURL = [sd CASServiceURL]; + if ([baseCASURL length]) + casActionURL = [baseCASURL composeURLWithAction: casAction + parameters: parameters + andHash: NO]; + else + { + [self errorWithFormat: + @"'SOGoCASServiceURL' is empty in the user defaults"]; + casActionURL = nil; + } + + return casActionURL; +} + ++ (SOGoCASSession *) CASSessionWithTicket: (NSString *) newTicket +{ + SOGoCASSession *newSession; + + if (newTicket) + { + newSession = [self new]; + [newSession autorelease]; + [newSession setTicket: newTicket]; + } + else + newSession = nil; + + return newSession; +} + ++ (SOGoCASSession *) CASSessionWithIdentifier: (NSString *) identifier +{ + SOGoCASSession *session; + SOGoCache *cache; + NSString *ticket; + + cache = [SOGoCache sharedCache]; + ticket = [cache CASTicketFromIdentifier: identifier]; + session = [self CASSessionWithTicket: ticket]; + + return session; +} + +- (id) init +{ + if ((self = [super init])) + { + ticket = nil; + login = nil; + pgt = nil; + identifier = nil; + proxyTickets = nil; + cacheUpdateNeeded = NO; + } + + return self; +} + +- (void) dealloc +{ + [login release]; + [pgt release]; + [ticket release]; + [proxyTickets release]; + [super dealloc]; +} + +- (void) _loadSessionFromCache +{ + SOGoCache *cache; + NSString *jsonSession; + NSDictionary *sessionDict; + + cache = [SOGoCache sharedCache]; + jsonSession = [cache CASSessionWithTicket: ticket]; + if ([jsonSession length]) + { + sessionDict = [NSMutableDictionary dictionaryWithJSONString: jsonSession]; + ASSIGN (login, [sessionDict objectForKey: @"login"]); + ASSIGN (pgt, [sessionDict objectForKey: @"pgt"]); + ASSIGN (identifier, [sessionDict objectForKey: @"identifier"]); + ASSIGN (proxyTickets, [sessionDict objectForKey: @"proxyTickets"]); + if (!proxyTickets) + proxyTickets = [NSMutableDictionary new]; + } + else + cacheUpdateNeeded = YES; +} + +- (void) _saveSessionToCache +{ + SOGoCache *cache; + NSString *jsonSession; + NSMutableDictionary *sessionDict; + + cache = [SOGoCache sharedCache]; + sessionDict = [NSMutableDictionary dictionary]; + [sessionDict setObject: login forKey: @"login"]; + if (pgt) + [sessionDict setObject: pgt forKey: @"pgt"]; + [sessionDict setObject: identifier forKey: @"identifier"]; + if ([proxyTickets count]) + [sessionDict setObject: proxyTickets forKey: @"proxyTickets"]; + jsonSession = [sessionDict jsonStringValue]; + [cache setCASSession: jsonSession + withTicket: ticket + forIdentifier: identifier]; +} + +- (void) setTicket: (NSString *) newTicket +{ + ASSIGN (ticket, newTicket); + [self _loadSessionFromCache]; +} + +- (NSString *) ticket +{ + return ticket; +} + +- (void) _parseSuccessElement: (NGDOMElement *) element +{ + NSString *tagName, *pgtIou; + NGDOMText *valueNode; + SOGoCache *cache; + + tagName = [element tagName]; + valueNode = (NGDOMText *) [element firstChild]; + if ([valueNode nodeType] == DOM_TEXT_NODE) + { + if ([tagName isEqualToString: @"user"]) + ASSIGN (login, [valueNode nodeValue]); + else if ([tagName isEqualToString: @"proxyGrantingTicket"]) + { + pgtIou = [valueNode nodeValue]; + cache = [SOGoCache sharedCache]; + ASSIGN (pgt, [cache CASPGTIdFromPGTIOU: pgtIou]); + } + else + [self logWithFormat: @"unhandled success tag '%@'", tagName]; + } +} + +- (void) _parseProxySuccessElement: (NGDOMElement *) element +{ + NSString *tagName; + NGDOMText *valueNode; + + tagName = [element tagName]; + if ([tagName isEqualToString: @"proxyTicket"]) + { + valueNode = (NGDOMText *) [element firstChild]; + if ([valueNode nodeType] == DOM_TEXT_NODE) + { + [proxyTickets setObject: [valueNode nodeValue] + forKey: currentProxyService]; + cacheUpdateNeeded = YES; + } + } + else + [self logWithFormat: @"unhandled proxy success tag '%@'", tagName]; +} + +- (void) _parseProxyFailureElement: (NGDOMElement *) element +{ + NSMutableString *errorString; + NSString *errorText; + NGDOMText *valueNode; + + errorString = [NSMutableString stringWithString: (@"a CAS failure occured" + @" during operation")]; + if ([element hasAttribute: @"code"]) + [errorString appendFormat: @" (code: '%@')", + [element attribute: @"code"]]; + valueNode = (NGDOMText *) [element firstChild]; + if (valueNode) + { + [errorString appendString: @":"]; + while (valueNode) + { + if ([valueNode nodeType] == DOM_TEXT_NODE) + { + errorText = [[valueNode nodeValue] stringByTrimmingSpaces]; + [errorString appendFormat: @" %@", errorText]; + } + valueNode = (NGDOMText *) [valueNode nextSibling]; + } + } + + [self logWithFormat: errorString]; +} + +- (SEL) _selectorForSubElementsOfTag: (NSString *) tag +{ + static NSMutableDictionary *mapping = nil; + NSString *methodName; + SEL selector; + + if (!mapping) + { + mapping = [NSMutableDictionary new]; + [mapping setObject: @"_parseSuccessElement:" + forKey: @"authenticationSuccess"]; + [mapping setObject: @"_parseProxySuccessElement:" + forKey: @"proxySuccess"]; + } + + methodName = [mapping objectForKey: tag]; + if (methodName) + selector = NSSelectorFromString (methodName); + else + { + selector = NULL; + [self errorWithFormat: @"unhandled response tag '%@'", tag]; + } + + return selector; +} + +- (void) _parseResponseElement: (NGDOMElement *) element +{ + id nodes; + NGDOMElement *currentNode; + SEL parseElementSelector; + NSString *tagName; + int count, max; + + tagName = [element tagName]; + if ([tagName isEqualToString: @"proxyFailure"]) + [self _parseProxyFailureElement: element]; + else + { + parseElementSelector = [self _selectorForSubElementsOfTag: tagName]; + if (parseElementSelector) + { + nodes = [element childNodes]; + max = [nodes length]; + for (count = 0; count < max; count++) + { + currentNode = [nodes objectAtIndex: count]; + if ([currentNode nodeType] == DOM_ELEMENT_NODE) + [self performSelector: parseElementSelector + withObject: currentNode]; + } + } + } +} + +- (void) _parseDOMResponse: (NGDOMDocument *) response +{ + NGDOMElement *top; + id nodes; + NGDOMElement *currentNode; + int count, max; + + top = [response documentElement]; + nodes = [top childNodes]; + max = [nodes length]; + for (count = 0; count < max; count++) + { + currentNode = [nodes objectAtIndex: count]; + if ([currentNode nodeType] == DOM_ELEMENT_NODE) + [self _parseResponseElement: currentNode]; + } +} + +- (void) _performCASRequestWithAction: (NSString *) casAction + andParameters: (NSDictionary *) parameters +{ + NSString *requestURL; + NSURL *url; + WORequest *request; + WOResponse *response; + WOHTTPConnection *httpConnection; + + requestURL = [[self class] CASURLWithAction: casAction + andParameters: parameters]; + if (requestURL) + { + url = [NSURL URLWithString: requestURL]; + httpConnection = [[WOHTTPConnection alloc] + initWithURL: url]; + [httpConnection autorelease]; + request = [[WORequest alloc] initWithMethod: @"GET" + uri: [requestURL hostlessURL] + httpVersion: @"HTTP/1.1" + headers: nil content: nil + userInfo: nil]; + [request autorelease]; + [httpConnection sendRequest: request]; + response = [httpConnection readResponse]; + [self _parseDOMResponse: [response contentAsDOMDocument]]; + } +} + +/* returns the URL that matches -[SOGoRootPage casProxyAction] */ +- (NSString *) _pgtUrlFromURL: (NSURL *) soURL +{ + WOApplication *application; + NSString *pgtURL; + WORequest *request; + + application = [WOApplication application]; + request = [[application context] request]; + pgtURL = [NSString stringWithFormat: + @"https://%@/%@/casProxy", + [soURL host], [request applicationName]]; + + return pgtURL; +} + +- (void) _fetchTicketData +{ + NSDictionary *params; + NSURL *soURL; + NSString *serviceURL; + + soURL = [[WOApplication application] soURL]; + serviceURL = [soURL absoluteString]; + + params = [NSDictionary dictionaryWithObjectsAndKeys: + ticket, @"ticket", serviceURL, @"service", + [self _pgtUrlFromURL: soURL], @"pgtUrl", + nil]; + [self _performCASRequestWithAction: @"serviceValidate" + andParameters: params]; + identifier = [SOGoObject globallyUniqueObjectId]; + [identifier retain]; + + cacheUpdateNeeded = YES; +} + +- (NSString *) login +{ + if (!login) + [self _fetchTicketData]; + + return login; +} + +- (NSString *) identifier +{ + return identifier; +} + +- (void) updateCache +{ + if (cacheUpdateNeeded) + { + [self _saveSessionToCache]; + cacheUpdateNeeded = NO; + } +} + +- (void) _fetchTicketDataForService: (NSString *) service +{ + NSDictionary *params; + + params = [NSDictionary dictionaryWithObjectsAndKeys: + pgt, @"pgt", service, @"targetService", + nil]; + [self _performCASRequestWithAction: @"proxy" + andParameters: params]; +} + +- (NSString *) ticketForService: (NSString *) service +{ + NSString *proxyTicket; + + if (pgt) + { + proxyTicket = [proxyTickets objectForKey: service]; + if (!proxyTicket) + { + currentProxyService = service; + [self _fetchTicketDataForService: service]; + proxyTicket = [proxyTickets objectForKey: service]; + if (proxyTicket) + cacheUpdateNeeded = YES; + currentProxyService = nil; + } + } + else + { + [self errorWithFormat: @"attempted to obtain a ticket for service '%@'" + @" while no PGT available", service]; + proxyTicket = nil; + } + + return proxyTicket; +} + +- (void) invalidateTicketForService: (NSString *) service +{ + [proxyTickets removeObjectForKey: service]; + cacheUpdateNeeded = YES; +} + +@end diff --git a/SoObjects/SOGo/SOGoCache.h b/SoObjects/SOGo/SOGoCache.h index 74ba8b152..07690c285 100644 --- a/SoObjects/SOGo/SOGoCache.h +++ b/SoObjects/SOGo/SOGoCache.h @@ -62,6 +62,11 @@ withName: (NSString *) userName; - (id) userNamed: (NSString *) name; +/* NSDictionary-like methods */ +- (void) setValue: (NSString *) value forKey: (NSString *) key; +- (NSString *) valueForKey: (NSString *) key; +- (void) removeValueForKey: (NSString *) key; + - (void) setUserAttributes: (NSString *) attributes forLogin: (NSString *) login; - (NSString *) userAttributesForLogin: (NSString *) theLogin; @@ -74,6 +79,17 @@ forLogin: (NSString *) login; - (NSString *) userSettingsForLogin: (NSString *) theLogin; +/* CAS support */ +- (NSString *) CASTicketFromIdentifier: (NSString *) identifier; +- (NSString *) CASSessionWithTicket: (NSString *) ticket; +- (void) setCASSession: (NSString *) casSession + withTicket: (NSString *) ticket + forIdentifier: (NSString *) identifier; + +- (NSString *) CASPGTIdFromPGTIOU: (NSString *) pgtIou; +- (void) setCASPGTId: (NSString *) pgtId + forPGTIOU: (NSString *) pgtIou; + @end #endif /* SOGOCACHE_H */ diff --git a/SoObjects/SOGo/SOGoCache.m b/SoObjects/SOGo/SOGoCache.m index 8cb9a6dc3..fee11291c 100644 --- a/SoObjects/SOGo/SOGoCache.m +++ b/SoObjects/SOGo/SOGoCache.m @@ -119,8 +119,8 @@ - (void) dealloc { - memcached_server_free(servers); - memcached_free(handle); + memcached_server_free (servers); + memcached_free (handle); [memcachedServerName release]; [cache release]; [users release]; @@ -203,73 +203,124 @@ // For non-blocking cache method, see memcached_behavior_set and MEMCACHED_BEHAVIOR_NO_BLOCK // memcached is thread-safe so no need to lock here. // -- (void) _cacheValues: (NSString *) theAttributes - ofType: (NSString *) theType - forLogin: (NSString *) theLogin +- (void) setValue: (NSString *) value + forKey: (NSString *) key + expire: (float) expiration { + NSData *keyData, *valueData; memcached_return error; - NSString *keyName; - NSData *key, *value; - keyName = [NSString stringWithFormat: @"%@+%@", theLogin, theType]; if (handle) { - key = [keyName dataUsingEncoding: NSUTF8StringEncoding]; - value = [theAttributes dataUsingEncoding: NSUTF8StringEncoding]; - error = memcached_set(handle, - [key bytes], [key length], - [value bytes], [value length], - cleanupInterval, 0); - [localCache setObject: theAttributes forKey: keyName]; - + keyData = [key dataUsingEncoding: NSUTF8StringEncoding]; + valueData = [value dataUsingEncoding: NSUTF8StringEncoding]; + error = memcached_set (handle, + [keyData bytes], [keyData length], + [valueData bytes], [valueData length], + expiration, 0); if (error != MEMCACHED_SUCCESS) - [self logWithFormat: @"memcached error: unable to cache values with subtype '%@' for user '%@'", theType, theLogin]; + [self logWithFormat: + @"memcached error: unable to cache values for key '%@'", + key]; //else //[self logWithFormat: @"memcached: cached values (%s) with subtype %@ //for user %@", value, theType, theLogin]; } else - [self errorWithFormat: @"attempting to cache value for key '%@' while" - " no handle exists", keyName]; + [self errorWithFormat: (@"attempting to cache value for key '%@' while" + " no handle exists"), key]; +} + +- (void) setValue: (NSString *) value + forKey: (NSString *) key +{ + [self setValue: value forKey: key + expire: cleanupInterval]; +} + +- (NSString *) valueForKey: (NSString *) key +{ + NSString *valueString; + NSData *keyData; + char *value; + size_t vlen; + memcached_return rc; + unsigned int flags; + + if (handle) + { + keyData = [key dataUsingEncoding: NSUTF8StringEncoding]; + value = memcached_get (handle, [keyData bytes], [keyData length], + &vlen, &flags, &rc); + if (rc == MEMCACHED_SUCCESS && value) + { + valueString + = [[NSString alloc] initWithBytesNoCopy: value + length: vlen + encoding: NSUTF8StringEncoding + freeWhenDone: YES]; + [valueString autorelease]; + } + else + valueString = nil; + } + else + { + valueString = nil; + [self errorWithFormat: @"attempting to retrieved cached value for key" + @" '%@' while no handle exists", key]; + } + + return valueString; +} + +- (void) removeValueForKey: (NSString *) key +{ + NSData *keyData; + memcached_return rc; + + if (handle) + { + keyData = [key dataUsingEncoding: NSUTF8StringEncoding]; + rc = memcached_delete (handle, [keyData bytes], [keyData length], + 0); + if (rc != MEMCACHED_SUCCESS) + [self errorWithFormat: (@"failure deleting cached value for key" + @" '%@'"), + key]; + } + else + [self errorWithFormat: (@"attempting to delete cached value for key" + @" '%@' while no handle exists"), + key]; +} + +- (void) _cacheValues: (NSString *) theAttributes + ofType: (NSString *) theType + forLogin: (NSString *) theLogin +{ + NSString *keyName; + + keyName = [NSString stringWithFormat: @"%@+%@", theLogin, theType]; + [self setValue: theAttributes forKey: keyName]; + [localCache setObject: theAttributes forKey: keyName]; } - (NSString *) _valuesOfType: (NSString *) theType forLogin: (NSString *) theLogin { NSString *valueString, *keyName; - NSData *key; - char *value; - size_t vlen; - memcached_return rc; - unsigned int flags; valueString = nil; keyName = [NSString stringWithFormat: @"%@+%@", theLogin, theType]; - if (handle) + valueString = [localCache objectForKey: keyName]; + if (!valueString) { - valueString = [localCache objectForKey: keyName]; - if (!valueString) - { - key = [keyName dataUsingEncoding: NSUTF8StringEncoding]; - value = memcached_get (handle, [key bytes], [key length], - &vlen, &flags, &rc); - if (rc == MEMCACHED_SUCCESS && value) - { - valueString - = [[NSString alloc] initWithBytesNoCopy: value - length: vlen - encoding: NSUTF8StringEncoding - freeWhenDone: YES]; - [valueString autorelease]; - // Cache the value in our localCache - [localCache setObject: valueString forKey: keyName]; - } - } + valueString = [self valueForKey: keyName]; + if (valueString) + [localCache setObject: valueString forKey: keyName]; } - else - [self errorWithFormat: @"attempting to retrieved cached value for key" - @" '%@' while no handle exists", keyName]; return valueString; } @@ -310,4 +361,47 @@ return [self _valuesOfType: @"settings" forLogin: theLogin]; } +/* CAS session support */ +- (NSString *) CASTicketFromIdentifier: (NSString *) identifier +{ + return [self valueForKey: [NSString stringWithFormat: @"cas-id:%@", + identifier]]; +} + +- (NSString *) CASSessionWithTicket: (NSString *) ticket +{ + return [self valueForKey: [NSString stringWithFormat: @"cas-ticket:%@", + ticket]]; +} + +- (void) setCASSession: (NSString *) casSession + withTicket: (NSString *) ticket + forIdentifier: (NSString *) identifier +{ + [self setValue: ticket + forKey: [NSString stringWithFormat: @"cas-id:%@", identifier]]; + [self setValue: casSession + forKey: [NSString stringWithFormat: @"cas-ticket:%@", ticket]]; +} + +- (NSString *) CASPGTIdFromPGTIOU: (NSString *) pgtIou +{ + NSString *casPgtId, *key; + + key = [NSString stringWithFormat: @"cas-pgtiou:%@", pgtIou]; + casPgtId = [self valueForKey: key]; + /* we directly remove the value as it can only be used once anyway */ + if (casPgtId) + [self removeValueForKey: key]; + + return casPgtId; +} + +- (void) setCASPGTId: (NSString *) pgtId + forPGTIOU: (NSString *) pgtIou +{ + [self setValue: pgtId + forKey: [NSString stringWithFormat: @"cas-pgtiou:%@", pgtIou]]; +} + @end diff --git a/SoObjects/SOGo/SOGoDAVAuthenticator.m b/SoObjects/SOGo/SOGoDAVAuthenticator.m index aaf2f1ceb..ea7d0823e 100644 --- a/SoObjects/SOGo/SOGoDAVAuthenticator.m +++ b/SoObjects/SOGo/SOGoDAVAuthenticator.m @@ -69,6 +69,13 @@ return password; } +- (NSString *) imapPasswordInContext: (WOContext *) context + forServer: (NSString *) imapServer + forceRenew: (BOOL) renew +{ + return [self passwordInContext: context]; +} + /* create SOGoUser */ - (SOGoUser *) userInContext: (WOContext *)_ctx diff --git a/SoObjects/SOGo/SOGoObject.m b/SoObjects/SOGo/SOGoObject.m index 69fccd093..35ec06c85 100644 --- a/SoObjects/SOGo/SOGoObject.m +++ b/SoObjects/SOGo/SOGoObject.m @@ -129,7 +129,7 @@ f = [[NSDate date] timeIntervalSince1970]; return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X", - pid, (int) f, sequence++, random]; + pid, (int) f, sequence++, (int) rndm]; } - (NSString *) globallyUniqueObjectId diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 88547db62..a79cdc16e 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -60,6 +60,10 @@ - (NSArray *) supportedLanguages; - (NSString *) loginSuffix; +- (NSString *) authenticationType; + +- (NSString *) CASServiceURL; + @end #endif /* SOGOSYSTEMDEFAULTS_H */ diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index a70c58a7f..7a146d2b9 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -285,4 +285,14 @@ BootstrapNSUserDefaults () return [self stringForKey: @"SOGoLoginSuffix"]; } +- (NSString *) authenticationType +{ + return [[self stringForKey: @"SOGoAuthenticationType"] lowercaseString]; +} + +- (NSString *) CASServiceURL +{ + return [self stringForKey: @"SOGoCASServiceURL"]; +} + @end diff --git a/SoObjects/SOGo/SOGoWebAuthenticator.m b/SoObjects/SOGo/SOGoWebAuthenticator.m index f31f7635b..7e49d6b93 100644 --- a/SoObjects/SOGo/SOGoWebAuthenticator.m +++ b/SoObjects/SOGo/SOGoWebAuthenticator.m @@ -36,9 +36,11 @@ #import -#import "SOGoUserManager.h" +#import "SOGoCASSession.h" #import "SOGoPermissions.h" +#import "SOGoSystemDefaults.h" #import "SOGoUser.h" +#import "SOGoUserManager.h" #import "SOGoWebAuthenticator.h" @@ -57,8 +59,24 @@ - (BOOL) checkLogin: (NSString *) _login password: (NSString *) _pwd { - return [[SOGoUserManager sharedUserManager] checkLogin: _login + SOGoSystemDefaults *sd; + BOOL rc; + SOGoCASSession *session; + + sd = [SOGoSystemDefaults sharedSystemDefaults]; + if ([[sd authenticationType] isEqualToString: @"cas"]) + { + session = [SOGoCASSession CASSessionWithIdentifier: _pwd]; + if (session) + rc = [[session login] isEqualToString: _login]; + else + rc = NO; + } + else + rc = [[SOGoUserManager sharedUserManager] checkLogin: _login andPassword: _pwd]; + + return rc; } - (SOGoUser *) userInContext: (WOContext *)_ctx @@ -83,7 +101,7 @@ { NSArray *creds; NSString *auth, *password; - + auth = [[context request] cookieValueForKey: [self cookieNameInContext: context]]; creds = [self parseCredentials: auth]; @@ -95,6 +113,33 @@ return password; } +- (NSString *) imapPasswordInContext: (WOContext *) context + forServer: (NSString *) imapServer + forceRenew: (BOOL) renew +{ + SOGoSystemDefaults *sd; + SOGoCASSession *session; + NSString *password, *service; + + password = [self passwordInContext: context]; + if ([password length]) + { + sd = [SOGoSystemDefaults sharedSystemDefaults]; + if ([[sd authenticationType] isEqualToString: @"cas"]) + { + session = [SOGoCASSession CASSessionWithIdentifier: password]; + service = [NSString stringWithFormat: @"imap://%@", imapServer]; + if (renew) + [session invalidateTicketForService: service]; + password = [session ticketForService: service]; + if ([password length] || renew) + [session updateCache]; + } + } + + return password; +} + /* create SOGoUser */ - (SOGoUser *) userWithLogin: (NSString *) login @@ -113,7 +158,7 @@ */ WOResponse *response; NSString *auth; - + auth = [[context request] cookieValueForKey: [self cookieNameInContext:context]]; if ([auth isEqualToString: @"discard"]) @@ -133,16 +178,20 @@ inContext: (WOContext *) context { WOComponent *page; + WORequest *request; WOCookie *authCookie; NSCalendarDate *date; + NSString *appName; + request = [context request]; page = [[WOApplication application] pageWithName: @"SOGoRootPage" - forRequest: [context request]]; - [[SoDefaultRenderer sharedRenderer] renderObject: page + forRequest: request]; + [[SoDefaultRenderer sharedRenderer] renderObject: [page defaultAction] inContext: context]; authCookie = [WOCookie cookieWithName: [self cookieNameInContext: context] value: @"discard"]; - [authCookie setPath: @"/"]; + appName = [request applicationName]; + [authCookie setPath: [NSString stringWithFormat: @"/%@/", appName]]; date = [NSCalendarDate calendarDate]; [authCookie setExpires: [date yesterday]]; [response addCookie: authCookie]; diff --git a/UI/Common/UIxPageFrame.m b/UI/Common/UIxPageFrame.m index 7a4166364..a9208f303 100644 --- a/UI/Common/UIxPageFrame.m +++ b/UI/Common/UIxPageFrame.m @@ -409,10 +409,15 @@ { BOOL canLogoff; id auth; + SOGoSystemDefaults *sd; auth = [[self clientObject] authenticatorInContext: context]; if ([auth respondsToSelector: @selector (cookieNameInContext:)]) - canLogoff = ([[auth cookieNameInContext: context] length] > 0); + { + sd = [SOGoSystemDefaults sharedSystemDefaults]; + canLogoff = ([[auth cookieNameInContext: context] length] > 0 + && ![[sd authenticationType] isEqualToString: @"cas"]); + } else canLogoff = NO; diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index fc7584f06..9ea078371 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -19,8 +19,11 @@ 02111-1307, USA. */ +#import #import +#import +#import #import #import #import @@ -32,6 +35,9 @@ #import #import +#import +#import +#import #import #import #import @@ -54,6 +60,27 @@ return [NSString stringWithFormat: @"%@/connect", [self applicationPath]]; } +- (WOCookie *) _cookieWithUsername: (NSString *) username + andPassword: (NSString *) password + forAuthenticator: (SOGoWebAuthenticator *) auth +{ + WOCookie *authCookie; + NSString *cookieValue, *cookieString, *appName; + + cookieString = [NSString stringWithFormat: @"%@:%@", + username, password]; + cookieValue = [NSString stringWithFormat: @"basic %@", + [cookieString stringByEncodingBase64]]; + authCookie = [WOCookie cookieWithName: [auth cookieNameInContext: context] + value: cookieValue]; + appName = [[context request] applicationName]; + [authCookie setPath: [NSString stringWithFormat: @"/%@/", appName]]; + /* enable this when we have code to determine whether request is HTTPS: + [authCookie setIsSecure: YES]; */ + + return authCookie; +} + /* actions */ - (id ) connectAction { @@ -62,70 +89,154 @@ WOCookie *authCookie; SOGoWebAuthenticator *auth; SOGoUserDefaults *ud; - NSString *cookieValue, *cookieString; - NSString *userName, *password, *language; + NSString *username, *password, *language; NSArray *supportedLanguages; auth = [[WOApplication application] authenticatorInContext: context]; request = [context request]; - userName = [request formValueForKey: @"userName"]; + username = [request formValueForKey: @"userName"]; password = [request formValueForKey: @"password"]; language = [request formValueForKey: @"language"]; - if ([auth checkLogin: userName password: password]) + if ([auth checkLogin: username password: password]) { - [self logWithFormat: @"successful login for user '%@'", userName]; + [self logWithFormat: @"successful login for user '%@'", username]; response = [self responseWith204]; - cookieString = [NSString stringWithFormat: @"%@:%@", - userName, password]; - cookieValue = [NSString stringWithFormat: @"basic %@", - [cookieString stringByEncodingBase64]]; - authCookie = [WOCookie cookieWithName: [auth cookieNameInContext: context] - value: cookieValue]; - [authCookie setPath: @"/"]; - /* enable this when we have code to determine whether request is HTTPS: - [authCookie setIsSecure: YES]; */ + authCookie = [self _cookieWithUsername: username andPassword: password + forAuthenticator: auth]; [response addCookie: authCookie]; supportedLanguages = [[SOGoSystemDefaults sharedSystemDefaults] supportedLanguages]; if (language && [supportedLanguages containsObject: language]) { - ud = [[SOGoUser userWithLogin: userName roles: nil] - userDefaults]; + ud = [[SOGoUser userWithLogin: username] userDefaults]; [ud setLanguage: language]; [ud synchronize]; } } else { - [self logWithFormat: @"failed login for user '%@'", userName]; + [self logWithFormat: @"failed login for user '%@'", username]; response = [self responseWithStatus: 403]; } return response; } -- (id ) defaultAction +- (NSDictionary *) _casRedirectKeys { - id response; + NSDictionary *redirectKeys; + NSURL *soURL; + + soURL = [[WOApplication application] soURL]; + + redirectKeys = [NSDictionary dictionaryWithObject: [soURL absoluteString] + forKey: @"service"]; + + return redirectKeys; +} + +- (id ) casProxyAction +{ + SOGoCache *cache; + WORequest *request; + NSString *pgtId, *pgtIou; + + request = [context request]; + pgtId = [request formValueForKey: @"pgtId"]; + pgtIou = [request formValueForKey: @"pgtIou"]; + if ([pgtId length] && [pgtIou length]) + { + cache = [SOGoCache sharedCache]; + [cache setCASPGTId: pgtId forPGTIOU: pgtIou]; + } + + return [self responseWithStatus: 200]; +} + +- (id ) _casDefaultAction +{ + WOResponse *response; + NSString *login, *newLocation, *oldLocation, *ticket; + SOGoCASSession *casSession; + SOGoWebAuthenticator *auth; + WOCookie *casCookie; + + casCookie = nil; + + login = [[context activeUser] login]; + if ([login isEqualToString: @"anonymous"]) + login = nil; + if (!login) + { + ticket = [[context request] formValueForKey: @"ticket"]; + if ([ticket length]) + { + casSession = [SOGoCASSession CASSessionWithTicket: ticket]; + login = [casSession login]; + if ([login length]) + { + auth = [[WOApplication application] + authenticatorInContext: context]; + casCookie = [self _cookieWithUsername: login + andPassword: [casSession identifier] + forAuthenticator: auth]; + [casSession updateCache]; + } + } + } + + if (login) + { + oldLocation = [[self clientObject] baseURLInContext: context]; + newLocation = [NSString stringWithFormat: @"%@%@", + oldLocation, [login stringByEscapingURL]]; + } + else + newLocation = [SOGoCASSession CASURLWithAction: @"login" + andParameters: [self _casRedirectKeys]]; + response = [self redirectToLocation: newLocation]; + if (casCookie) + [response addCookie: casCookie]; + + return response; +} + +- (id ) _standardDefaultAction +{ + NSObject *response; NSString *login, *oldLocation; login = [[context activeUser] login]; - if (!login || [login isEqualToString: @"anonymous"]) - response = self; - else + if ([login isEqualToString: @"anonymous"]) + login = nil; + + if (login) { oldLocation = [[self clientObject] baseURLInContext: context]; response - = [self redirectToLocation: [NSString stringWithFormat: @"%@/%@", + = [self redirectToLocation: [NSString stringWithFormat: @"%@%@", oldLocation, [login stringByEscapingURL]]]; } + else + response = self; return response; } +- (id ) defaultAction +{ + SOGoSystemDefaults *sd; + + sd = [SOGoSystemDefaults sharedSystemDefaults]; + + return ([[sd authenticationType] isEqualToString: @"cas"] + ? [self _casDefaultAction] + : [self _standardDefaultAction]); +} + - (BOOL) isPublicInContext: (WOContext *) localContext { return YES; diff --git a/UI/MainUI/SOGoUserHomePage.m b/UI/MainUI/SOGoUserHomePage.m index 37a47d4dd..83b3c0d16 100644 --- a/UI/MainUI/SOGoUserHomePage.m +++ b/UI/MainUI/SOGoUserHomePage.m @@ -224,8 +224,6 @@ WOResponse *response; response = [self responseWithStatus: 200]; -// [response setHeader: @"text/plain; charset=iso-8859-1" -// forKey: @"Content-Type"]; [response appendContentString: [self _freeBusyAsText]]; return response; @@ -238,7 +236,7 @@ SOGoWebAuthenticator *auth; id container; NSCalendarDate *date; - NSString *userName, *cookieName; + NSString *userName, *cookieName, *appName; container = [[self clientObject] container]; @@ -261,7 +259,8 @@ if ([cookieName length]) { cookie = [WOCookie cookieWithName: cookieName value: @"discard"]; - [cookie setPath: @"/"]; + appName = [[context request] applicationName]; + [cookie setPath: [NSString stringWithFormat: @"/%@/", appName]]; [cookie setExpires: [date yesterday]]; [response addCookie: cookie]; } diff --git a/UI/MainUI/product.plist b/UI/MainUI/product.plist index da77a845b..80ce5f22e 100644 --- a/UI/MainUI/product.plist +++ b/UI/MainUI/product.plist @@ -105,6 +105,11 @@ protectedBy = ""; pageName = "SOGoRootPage"; }; + casProxy = { + protectedBy = ""; + pageName = "SOGoRootPage"; + actionName = "casProxy"; + }; /* crash = { protectedBy = ""; pageName = "SOGoRootPage";