In multidomain environment right now we are trying to authenticate against all sources defined in sogo.conf because the domain is not set at this point. In sogo.conf we have to specify the domain a source is useful for, so with this patch instead of 'n' tries of authentication we will perform only 1 (in a scenario where we have 1 source per domain, and we have 'n' domains).
1095 lines
34 KiB
Objective-C
1095 lines
34 KiB
Objective-C
/* SOGoUserManager.m - this file is part of SOGo
|
|
*
|
|
* Copyright (C) 2007-2015 Inverse inc.
|
|
*
|
|
* 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 <Foundation/NSArray.h>
|
|
#import <Foundation/NSCalendarDate.h>
|
|
#import <Foundation/NSDictionary.h>
|
|
#import <Foundation/NSEnumerator.h>
|
|
#import <Foundation/NSException.h>
|
|
#import <Foundation/NSLock.h>
|
|
#import <Foundation/NSNull.h>
|
|
#import <Foundation/NSString.h>
|
|
#import <Foundation/NSTimer.h>
|
|
#import <Foundation/NSValue.h>
|
|
#import <NGExtensions/NSNull+misc.h>
|
|
#import <NGExtensions/NSObject+Logs.h>
|
|
|
|
#import "NSArray+Utilities.h"
|
|
#import "NSString+Utilities.h"
|
|
#import "NSString+Crypto.h"
|
|
#import "NSObject+Utilities.h"
|
|
#import "SOGoDomainDefaults.h"
|
|
#import "SOGoSource.h"
|
|
#import "SOGoSystemDefaults.h"
|
|
#import "SOGoUserManager.h"
|
|
#import "SOGoCache.h"
|
|
#import "SOGoConstants.h"
|
|
#import "SOGoSource.h"
|
|
|
|
static Class NSNullK;
|
|
|
|
@implementation SOGoUserManagerRegistry
|
|
|
|
+ (void) initialize
|
|
{
|
|
NSNullK = [NSNull class];
|
|
}
|
|
|
|
+ (id) sharedRegistry
|
|
{
|
|
static id sharedRegistry = nil;
|
|
|
|
if (!sharedRegistry)
|
|
sharedRegistry = [self new];
|
|
|
|
return sharedRegistry;
|
|
}
|
|
|
|
- (NSString *) sourceClassForType: (NSString *) type
|
|
{
|
|
NSString *sourceClass;
|
|
|
|
if (type)
|
|
{
|
|
if ([type isEqualToString: @"ldap"])
|
|
sourceClass = @"LDAPSource";
|
|
else if ([type isEqualToString: @"sql"])
|
|
sourceClass = @"SQLSource";
|
|
else
|
|
{
|
|
[NSException raise: @"SOGoUserManagerRegistryException"
|
|
format: @"No class known for type '%@'", type];
|
|
sourceClass = nil;
|
|
}
|
|
}
|
|
else
|
|
sourceClass = @"LDAPSource";
|
|
|
|
return sourceClass;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation SOGoUserManager
|
|
|
|
+ (id) sharedUserManager
|
|
{
|
|
static id sharedUserManager = nil;
|
|
|
|
if (!sharedUserManager)
|
|
sharedUserManager = [self new];
|
|
|
|
return sharedUserManager;
|
|
}
|
|
|
|
- (BOOL) _registerSource: (NSDictionary *) udSource
|
|
inDomain: (NSString *) domain
|
|
{
|
|
NSString *sourceID, *value, *type;
|
|
NSMutableDictionary *metadata;
|
|
NSObject <SOGoSource> *sogoSource;
|
|
BOOL isAddressBook, result;
|
|
Class c;
|
|
|
|
result = NO;
|
|
sourceID = [udSource objectForKey: @"id"];
|
|
if ([sourceID length] > 0)
|
|
{
|
|
if ([_sourcesMetadata objectForKey: sourceID])
|
|
[self errorWithFormat: @"attempted to register a contact/user source"
|
|
@" with duplicated id (%@)", sourceID];
|
|
else
|
|
{
|
|
type = [[udSource objectForKey: @"type"] lowercaseString];
|
|
c = NSClassFromString([_registry sourceClassForType: type]);
|
|
sogoSource = [c sourceFromUDSource: udSource inDomain: domain];
|
|
if (sourceID)
|
|
[_sources setObject: sogoSource forKey: sourceID];
|
|
else
|
|
[self errorWithFormat: @"id field missing in an user source,"
|
|
@" check the SOGoUserSources defaults"];
|
|
metadata = [NSMutableDictionary dictionary];
|
|
if (domain)
|
|
[metadata setObject: domain forKey: @"domain"];
|
|
value = [udSource objectForKey: @"canAuthenticate"];
|
|
if (value)
|
|
[metadata setObject: value forKey: @"canAuthenticate"];
|
|
value = [udSource objectForKey: @"isAddressBook"];
|
|
if (value)
|
|
{
|
|
[metadata setObject: value forKey: @"isAddressBook"];
|
|
isAddressBook = [value boolValue];
|
|
}
|
|
else
|
|
isAddressBook = NO;
|
|
value = [udSource objectForKey: @"displayName"];
|
|
if (value)
|
|
[metadata setObject: value forKey: @"displayName"];
|
|
else
|
|
{
|
|
if (isAddressBook)
|
|
[self errorWithFormat: @"addressbook source '%@' has"
|
|
@" no displayname", sourceID];
|
|
}
|
|
value = [udSource objectForKey: @"MailFieldNames"];
|
|
if (value)
|
|
[metadata setObject: value forKey: @"MailFieldNames"];
|
|
value = [udSource objectForKey: @"SearchFieldNames"];
|
|
if (value)
|
|
[metadata setObject: value forKey: @"SearchFieldNames"];
|
|
|
|
[_sourcesMetadata setObject: metadata forKey: sourceID];
|
|
result = YES;
|
|
}
|
|
}
|
|
else
|
|
[self errorWithFormat: @"attempted to register a contact/user source"
|
|
@" without id (skipped)"];
|
|
|
|
return result;
|
|
}
|
|
|
|
- (int) _registerSourcesInDomain: (NSString *) domain
|
|
{
|
|
NSArray *userSources;
|
|
unsigned int count, max, total;
|
|
SOGoDomainDefaults *dd;
|
|
|
|
if (domain)
|
|
dd = [SOGoDomainDefaults defaultsForDomain: domain];
|
|
else
|
|
dd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
|
|
userSources = [dd userSources];
|
|
max = [userSources count];
|
|
total = 0;
|
|
for (count = 0; count < max; count++)
|
|
if ([self _registerSource: [userSources objectAtIndex: count]
|
|
inDomain: domain])
|
|
total++;
|
|
|
|
return total;
|
|
}
|
|
|
|
- (void) _prepareSources
|
|
{
|
|
NSArray *domains;
|
|
unsigned int count, max, total;
|
|
|
|
_sources = [[NSMutableDictionary alloc] init];
|
|
_sourcesMetadata = [[NSMutableDictionary alloc] init];
|
|
|
|
total = [self _registerSourcesInDomain: nil];
|
|
domains = [[SOGoSystemDefaults sharedSystemDefaults] domainIds];
|
|
max = [domains count];
|
|
for (count = 0; count < max; count++)
|
|
total += [self _registerSourcesInDomain: [domains objectAtIndex: count]];
|
|
|
|
if (!total)
|
|
[self errorWithFormat: @"No authentication sources defined - nobody will be able to login. Check your defaults."];
|
|
}
|
|
|
|
- (id) init
|
|
{
|
|
if ((self = [super init]))
|
|
{
|
|
_sources = nil;
|
|
_sourcesMetadata = nil;
|
|
_registry = [NSClassFromString ([self registryClass]) sharedRegistry];
|
|
[self _prepareSources];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void) dealloc
|
|
{
|
|
[_sources release];
|
|
[_sourcesMetadata release];
|
|
[super dealloc];
|
|
}
|
|
|
|
- (NSString *) registryClass
|
|
{
|
|
return @"SOGoUserManagerRegistry";
|
|
}
|
|
|
|
- (NSArray *) sourceIDsInDomain: (NSString *) domain
|
|
{
|
|
NSMutableArray *sourceIDs;
|
|
NSArray *keys;
|
|
int count, max;
|
|
NSString *currentID, *sourceDomain;
|
|
NSObject <SOGoSource> *currentSource;
|
|
|
|
keys = [_sources allKeys];
|
|
max = [keys count];
|
|
sourceIDs = [NSMutableArray arrayWithCapacity: max];
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
currentID = [keys objectAtIndex: count];
|
|
currentSource = [_sources objectForKey: currentID];
|
|
sourceDomain = [currentSource domain];
|
|
if (![sourceDomain length] || [sourceDomain isEqualToString: domain])
|
|
[sourceIDs addObject: currentID];
|
|
}
|
|
|
|
return sourceIDs;
|
|
}
|
|
|
|
- (NSObject <SOGoSource> *) sourceWithID: (NSString *) sourceID
|
|
{
|
|
return [_sources objectForKey: sourceID];
|
|
}
|
|
|
|
- (NSDictionary *) metadataForSourceID: (NSString *) sourceID
|
|
{
|
|
return [_sourcesMetadata objectForKey: sourceID];
|
|
}
|
|
|
|
- (NSArray *) authenticationSourceIDsInDomain: (NSString *) domain
|
|
{
|
|
NSMutableArray *sourceIDs;
|
|
NSEnumerator *allIDs;
|
|
NSString *currentID, *sourceDomain;
|
|
NSDictionary *metadata;
|
|
|
|
sourceIDs = [NSMutableArray array];
|
|
allIDs = [[_sources allKeys] objectEnumerator];
|
|
while ((currentID = [allIDs nextObject]))
|
|
{
|
|
sourceDomain = [[_sources objectForKey: currentID] domain];
|
|
if (![domain length] || ![sourceDomain length] || [domain isEqualToString: sourceDomain])
|
|
{
|
|
metadata = [_sourcesMetadata objectForKey: currentID];
|
|
if ([[metadata objectForKey: @"canAuthenticate"] boolValue])
|
|
[sourceIDs addObject: currentID];
|
|
}
|
|
}
|
|
|
|
return sourceIDs;
|
|
}
|
|
|
|
- (NSArray *) addressBookSourceIDsInDomain: (NSString *) domain
|
|
{
|
|
NSMutableArray *sourceIDs;
|
|
NSEnumerator *allIDs;
|
|
NSString *currentID;
|
|
NSDictionary *metadata;
|
|
|
|
sourceIDs = [NSMutableArray array];
|
|
allIDs = [[self sourceIDsInDomain: domain] objectEnumerator];
|
|
while ((currentID = [allIDs nextObject]))
|
|
{
|
|
metadata = [_sourcesMetadata objectForKey: currentID];
|
|
if ([[metadata objectForKey: @"isAddressBook"] boolValue])
|
|
[sourceIDs addObject: currentID];
|
|
}
|
|
|
|
return sourceIDs;
|
|
}
|
|
|
|
- (NSString *) displayNameForSourceWithID: (NSString *) sourceID
|
|
{
|
|
NSDictionary *metadata;
|
|
|
|
metadata = [_sourcesMetadata objectForKey: sourceID];
|
|
|
|
return [metadata objectForKey: @"displayName"];
|
|
}
|
|
|
|
- (NSString *) getCNForUID: (NSString *) uid
|
|
{
|
|
NSDictionary *contactInfos;
|
|
|
|
// NSLog (@"getCNForUID: %@", uid);
|
|
contactInfos = [self contactInfosForUserWithUIDorEmail: uid];
|
|
|
|
return [contactInfos objectForKey: @"cn"];
|
|
}
|
|
|
|
- (NSString *) getEmailForUID: (NSString *) uid
|
|
{
|
|
NSDictionary *contactInfos;
|
|
|
|
// NSLog (@"getEmailForUID: %@", uid);
|
|
contactInfos = [self contactInfosForUserWithUIDorEmail: uid];
|
|
|
|
return [contactInfos objectForKey: @"c_email"];
|
|
}
|
|
|
|
- (NSString *) getFullEmailForUID: (NSString *) uid
|
|
{
|
|
NSDictionary *contactInfos;
|
|
NSString *cn, *email, *fullEmail;
|
|
|
|
contactInfos = [self contactInfosForUserWithUIDorEmail: uid];
|
|
email = [contactInfos objectForKey: @"c_email"];
|
|
cn = [contactInfos objectForKey: @"cn"];
|
|
if ([cn length] > 0)
|
|
{
|
|
if ([email length] > 0)
|
|
fullEmail = [NSString stringWithFormat: @"%@ <%@>",
|
|
cn, email];
|
|
else
|
|
fullEmail = cn;
|
|
}
|
|
else
|
|
fullEmail = email;
|
|
|
|
return fullEmail;
|
|
}
|
|
|
|
- (NSString *) getExternalLoginForUID: (NSString *) uid
|
|
inDomain: (NSString *) domain
|
|
{
|
|
NSDictionary *contactInfos;
|
|
NSString *login;
|
|
SOGoDomainDefaults *dd;
|
|
|
|
contactInfos = [self contactInfosForUserWithUIDorEmail: uid
|
|
inDomain: domain];
|
|
login = [contactInfos objectForKey: @"c_imaplogin"];
|
|
if (login == nil)
|
|
{
|
|
if ([domain length])
|
|
dd = [SOGoDomainDefaults defaultsForDomain: domain];
|
|
else
|
|
dd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
|
|
login = [dd forceExternalLoginWithEmail] ? [self getEmailForUID: uid] : uid;
|
|
}
|
|
|
|
return login;
|
|
}
|
|
|
|
- (NSString *) getUIDForEmail: (NSString *) email
|
|
{
|
|
NSDictionary *contactInfos;
|
|
|
|
contactInfos = [self contactInfosForUserWithUIDorEmail: email];
|
|
|
|
return [contactInfos objectForKey: @"c_uid"];
|
|
}
|
|
|
|
- (BOOL) _sourceChangePasswordForLogin: (NSString *) login
|
|
inDomain: (NSString *) domain
|
|
oldPassword: (NSString *) oldPassword
|
|
newPassword: (NSString *) newPassword
|
|
perr: (SOGoPasswordPolicyError *) perr
|
|
{
|
|
NSObject <SOGoSource> *sogoSource;
|
|
NSEnumerator *authIDs;
|
|
NSString *currentID;
|
|
BOOL didChange;
|
|
|
|
didChange = NO;
|
|
|
|
authIDs = [[self authenticationSourceIDsInDomain: domain] objectEnumerator];
|
|
while (!didChange && (currentID = [authIDs nextObject]))
|
|
{
|
|
sogoSource = [_sources objectForKey: currentID];
|
|
didChange = [sogoSource changePasswordForLogin: login
|
|
oldPassword: oldPassword
|
|
newPassword: newPassword
|
|
perr: perr];
|
|
}
|
|
|
|
return didChange;
|
|
}
|
|
|
|
- (BOOL) _sourceCheckLogin: (NSString *) login
|
|
andPassword: (NSString *) password
|
|
domain: (NSString **) domain
|
|
perr: (SOGoPasswordPolicyError *) perr
|
|
expire: (int *) expire
|
|
grace: (int *) grace
|
|
{
|
|
NSObject <SOGoSource> *sogoSource;
|
|
NSEnumerator *authIDs;
|
|
NSString *currentID;
|
|
BOOL checkOK;
|
|
SOGoSystemDefaults *sd;
|
|
NSRange r;
|
|
|
|
checkOK = NO;
|
|
|
|
if (*domain == nil)
|
|
{
|
|
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
r = [login rangeOfString: @"@" options: NSBackwardsSearch];
|
|
if ([sd enableDomainBasedUID] && r.location != NSNotFound)
|
|
*domain = [login substringFromIndex: (r.location + r.length)];
|
|
}
|
|
|
|
authIDs = [[self authenticationSourceIDsInDomain: *domain] objectEnumerator];
|
|
while (!checkOK && (currentID = [authIDs nextObject]))
|
|
{
|
|
sogoSource = [_sources objectForKey: currentID];
|
|
checkOK = [sogoSource checkLogin: login
|
|
password: password
|
|
perr: perr
|
|
expire: expire
|
|
grace: grace];
|
|
}
|
|
|
|
if (checkOK && *domain == nil)
|
|
*domain = [sogoSource domain];
|
|
|
|
return checkOK;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (BOOL) checkLogin: (NSString *) _login
|
|
password: (NSString *) _pwd
|
|
domain: (NSString **) _domain
|
|
perr: (SOGoPasswordPolicyError *) _perr
|
|
expire: (int *) _expire
|
|
grace: (int *) _grace
|
|
{
|
|
return [self checkLogin: _login
|
|
password: _pwd
|
|
domain: _domain
|
|
perr: _perr
|
|
expire: _expire
|
|
grace: _grace
|
|
useCache: YES];
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (BOOL) checkLogin: (NSString *) _login
|
|
password: (NSString *) _pwd
|
|
domain: (NSString **) _domain
|
|
perr: (SOGoPasswordPolicyError *) _perr
|
|
expire: (int *) _expire
|
|
grace: (int *) _grace
|
|
useCache: (BOOL) useCache
|
|
{
|
|
NSMutableDictionary *currentUser, *failedCount;
|
|
NSString *dictPassword, *username, *jsonUser;
|
|
SOGoSystemDefaults *dd;
|
|
BOOL checkOK;
|
|
|
|
// We check for cached passwords. If the entry is cached, we
|
|
// check this immediately. If not, we'll go directly at the
|
|
// authentication source and try to validate there, then cache it.
|
|
if (*_domain != nil)
|
|
username = [NSString stringWithFormat: @"%@@%@", _login, *_domain];
|
|
else
|
|
username = _login;
|
|
|
|
failedCount = [[SOGoCache sharedCache] failedCountForLogin: username];
|
|
dd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
|
|
//
|
|
// We check the fail count per user in memcache (per server). If the
|
|
// fail count reaches X in Y minutes, we deny immediately the
|
|
// authentications for Z minutes
|
|
//
|
|
if (failedCount)
|
|
{
|
|
unsigned int current_time, start_time, delta, block_time;
|
|
|
|
current_time = [[NSCalendarDate date] timeIntervalSince1970];
|
|
start_time = [[failedCount objectForKey: @"InitialDate"] unsignedIntValue];
|
|
delta = current_time - start_time;
|
|
|
|
block_time = [dd failedLoginBlockInterval];
|
|
|
|
if ([[failedCount objectForKey: @"FailedCount"] intValue] >= [dd maximumFailedLoginCount] &&
|
|
delta >= [dd maximumFailedLoginInterval] &&
|
|
delta <= block_time )
|
|
{
|
|
*_perr = PolicyAccountLocked;
|
|
return NO;
|
|
}
|
|
|
|
if (delta > block_time)
|
|
{
|
|
[[SOGoCache sharedCache] setFailedCount: 0
|
|
forLogin: username];
|
|
}
|
|
}
|
|
|
|
|
|
jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: username];
|
|
currentUser = [jsonUser objectFromJSONString];
|
|
dictPassword = [currentUser objectForKey: @"password"];
|
|
if (useCache && currentUser && dictPassword)
|
|
{
|
|
checkOK = ([dictPassword isEqualToString: [_pwd asSHA1String]]);
|
|
//NSLog(@"Password cache hit for user %@", _login);
|
|
}
|
|
else if ([self _sourceCheckLogin: _login
|
|
andPassword: _pwd
|
|
domain: _domain
|
|
perr: _perr
|
|
expire: _expire
|
|
grace: _grace])
|
|
{
|
|
checkOK = YES;
|
|
if (!currentUser)
|
|
{
|
|
currentUser = [NSMutableDictionary dictionary];
|
|
}
|
|
|
|
// It's important to cache the password here as we might have cached the
|
|
// user's entry in -contactInfosForUserWithUIDorEmail: and if we don't
|
|
// set the password and recache the entry, the password would never be
|
|
// cached for the user unless its entry expires from memcached's
|
|
// internal cache.
|
|
[currentUser setObject: [_pwd asSHA1String] forKey: @"password"];
|
|
[[SOGoCache sharedCache]
|
|
setUserAttributes: [currentUser jsonRepresentation]
|
|
forLogin: username];
|
|
}
|
|
else
|
|
{
|
|
// If failed login "rate-limiting" is enabled, we adjust the stats
|
|
if ([dd maximumFailedLoginCount])
|
|
{
|
|
[[SOGoCache sharedCache] setFailedCount: ([[failedCount objectForKey: @"FailedCount"] intValue] + 1)
|
|
forLogin: username];
|
|
}
|
|
|
|
checkOK = NO;
|
|
}
|
|
|
|
// We MUST, for all LDAP sources, update the bindDN and bindPassword
|
|
// to the user's value if bindAsCurrentUser is set to true in the
|
|
// LDAP source configuration
|
|
if (checkOK)
|
|
{
|
|
NSObject <SOGoDNSource> *currentSource;
|
|
NSEnumerator *sources;
|
|
|
|
sources = [[_sources allValues] objectEnumerator];
|
|
while ((currentSource = [sources nextObject]))
|
|
if ([currentSource conformsToProtocol: @protocol(SOGoDNSource)] &&
|
|
[currentSource bindAsCurrentUser] &&
|
|
[currentSource lookupDNByLogin: _login])
|
|
{
|
|
[currentSource setBindDN: [currentSource lookupDNByLogin: _login]];
|
|
[currentSource setBindPassword: _pwd];
|
|
}
|
|
}
|
|
|
|
return checkOK;
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (BOOL) changePasswordForLogin: (NSString *) login
|
|
inDomain: (NSString *) domain
|
|
oldPassword: (NSString *) oldPassword
|
|
newPassword: (NSString *) newPassword
|
|
perr: (SOGoPasswordPolicyError *) perr
|
|
{
|
|
NSString *jsonUser, *userLogin;
|
|
NSMutableDictionary *currentUser;
|
|
BOOL didChange;
|
|
SOGoSystemDefaults *sd;
|
|
|
|
jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: login];
|
|
currentUser = [jsonUser objectFromJSONString];
|
|
|
|
if ([self _sourceChangePasswordForLogin: login
|
|
inDomain: domain
|
|
oldPassword: oldPassword
|
|
newPassword: newPassword
|
|
perr: perr])
|
|
{
|
|
didChange = YES;
|
|
|
|
if (!currentUser)
|
|
currentUser = [NSMutableDictionary dictionary];
|
|
|
|
// It's important to cache the password here as we might have cached the
|
|
// user's entry in -contactInfosForUserWithUIDorEmail: and if we don't
|
|
// set the password and recache the entry, the password would never be
|
|
// cached for the user unless its entry expires from memcached's
|
|
// internal cache.
|
|
[currentUser setObject: [newPassword asSHA1String] forKey: @"password"];
|
|
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
if ([sd enableDomainBasedUID])
|
|
userLogin = [NSString stringWithFormat: @"%@@%@", login, domain];
|
|
else
|
|
userLogin = login;
|
|
[[SOGoCache sharedCache] setUserAttributes: [currentUser jsonRepresentation]
|
|
forLogin: userLogin];
|
|
}
|
|
else
|
|
didChange = NO;
|
|
|
|
return didChange;
|
|
}
|
|
|
|
- (void) _fillContactMailRecords: (NSMutableDictionary *) contact
|
|
{
|
|
NSString *uid, *domain, *systemEmail;
|
|
NSMutableArray *emails;
|
|
SOGoDomainDefaults *dd;
|
|
|
|
domain = [contact objectForKey: @"c_domain"];
|
|
if ([domain length])
|
|
dd = [SOGoDomainDefaults defaultsForDomain: domain];
|
|
else
|
|
dd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
emails = [contact objectForKey: @"emails"];
|
|
uid = [contact objectForKey: @"c_uid"];
|
|
if ([uid rangeOfString: @"@"].location == NSNotFound)
|
|
systemEmail
|
|
= [NSString stringWithFormat: @"%@@%@", uid, [dd mailDomain]];
|
|
else
|
|
systemEmail = uid;
|
|
|
|
// We always add the system email, which will always be returned
|
|
// by SOGoUser -systemEmail.
|
|
[emails addObject: systemEmail];
|
|
[contact setObject: [emails objectAtIndex: 0] forKey: @"c_email"];
|
|
}
|
|
|
|
//
|
|
//
|
|
//
|
|
- (void) _fillContactInfosForUser: (NSMutableDictionary *) currentUser
|
|
withUIDorEmail: (NSString *) uid
|
|
inDomain: (NSString *) domain
|
|
{
|
|
NSString *sourceID, *cn, *c_domain, *c_uid, *c_imaphostname, *c_imaplogin, *c_sievehostname;
|
|
NSObject <SOGoSource> *currentSource;
|
|
NSEnumerator *sogoSources;
|
|
NSDictionary *userEntry;
|
|
NSMutableArray *emails;
|
|
NSNumber *isGroup;
|
|
NSArray *c_emails;
|
|
BOOL access;
|
|
|
|
emails = [NSMutableArray array];
|
|
cn = nil;
|
|
c_uid = nil;
|
|
c_domain = nil;
|
|
c_imaphostname = nil;
|
|
c_imaplogin = nil;
|
|
c_sievehostname = nil;
|
|
|
|
[currentUser setObject: [NSNumber numberWithBool: YES]
|
|
forKey: @"CalendarAccess"];
|
|
[currentUser setObject: [NSNumber numberWithBool: YES]
|
|
forKey: @"MailAccess"];
|
|
|
|
sogoSources = [[self authenticationSourceIDsInDomain: domain] objectEnumerator];
|
|
userEntry = nil;
|
|
while (!userEntry && (sourceID = [sogoSources nextObject]))
|
|
{
|
|
currentSource = [_sources objectForKey: sourceID];
|
|
userEntry = [currentSource lookupContactEntryWithUIDorEmail: uid
|
|
inDomain: domain];
|
|
if (userEntry)
|
|
{
|
|
[currentUser setObject: sourceID forKey: @"SOGoSource"];
|
|
if (!cn)
|
|
cn = [userEntry objectForKey: @"c_cn"];
|
|
if (!c_uid)
|
|
c_uid = [userEntry objectForKey: @"c_uid"];
|
|
if (!c_domain)
|
|
c_domain = [userEntry objectForKey: @"c_domain"];
|
|
c_emails = [userEntry objectForKey: @"c_emails"];
|
|
if ([c_emails count])
|
|
[emails addObjectsFromArray: c_emails];
|
|
if (!c_imaphostname)
|
|
c_imaphostname = [userEntry objectForKey: @"c_imaphostname"];
|
|
if (!c_imaplogin)
|
|
c_imaplogin = [userEntry objectForKey: @"c_imaplogin"];
|
|
if (!c_sievehostname)
|
|
c_sievehostname = [userEntry objectForKey: @"c_sievehostname"];
|
|
access = [[userEntry objectForKey: @"CalendarAccess"] boolValue];
|
|
if (!access)
|
|
[currentUser setObject: [NSNumber numberWithBool: NO]
|
|
forKey: @"CalendarAccess"];
|
|
access = [[userEntry objectForKey: @"MailAccess"] boolValue];
|
|
if (!access)
|
|
[currentUser setObject: [NSNumber numberWithBool: NO]
|
|
forKey: @"MailAccess"];
|
|
|
|
// We check if it's a group
|
|
isGroup = [userEntry objectForKey: @"isGroup"];
|
|
if (isGroup)
|
|
[currentUser setObject: isGroup forKey: @"isGroup"];
|
|
|
|
// We also fill the resource attributes, if any
|
|
if ([userEntry objectForKey: @"isResource"])
|
|
[currentUser setObject: [userEntry objectForKey: @"isResource"]
|
|
forKey: @"isResource"];
|
|
if ([userEntry objectForKey: @"numberOfSimultaneousBookings"])
|
|
[currentUser setObject: [userEntry objectForKey: @"numberOfSimultaneousBookings"]
|
|
forKey: @"numberOfSimultaneousBookings"];
|
|
|
|
// This is Active Directory specific attribute (needed on OpenChange/* layer)
|
|
if ([userEntry objectForKey: @"samaccountname"])
|
|
[currentUser setObject: [userEntry objectForKey: @"samaccountname"]
|
|
forKey: @"sAMAccountName"];
|
|
}
|
|
}
|
|
|
|
if (!cn)
|
|
cn = @"";
|
|
if (!c_uid)
|
|
c_uid = @"";
|
|
if (!c_domain)
|
|
c_domain = @"";
|
|
|
|
if (c_imaphostname)
|
|
[currentUser setObject: c_imaphostname forKey: @"c_imaphostname"];
|
|
if (c_imaplogin)
|
|
[currentUser setObject: c_imaplogin forKey: @"c_imaplogin"];
|
|
if (c_sievehostname)
|
|
[currentUser setObject: c_sievehostname forKey: @"c_sievehostname"];
|
|
|
|
[currentUser setObject: emails forKey: @"emails"];
|
|
[currentUser setObject: cn forKey: @"cn"];
|
|
[currentUser setObject: c_uid forKey: @"c_uid"];
|
|
[currentUser setObject: c_domain forKey: @"c_domain"];
|
|
|
|
// If our LDAP queries gave us nothing, we add at least one default
|
|
// email address based on the default domain.
|
|
[self _fillContactMailRecords: currentUser];
|
|
}
|
|
|
|
//
|
|
// We cache here all identities, including those
|
|
// associated with email addresses.
|
|
//
|
|
- (void) _retainUser: (NSDictionary *) newUser
|
|
withLogin: (NSString *) login
|
|
{
|
|
NSEnumerator *emails;
|
|
NSString *key;
|
|
|
|
[[SOGoCache sharedCache]
|
|
setUserAttributes: [newUser jsonRepresentation]
|
|
forLogin: login];
|
|
if (![newUser isKindOfClass: NSNullK])
|
|
{
|
|
key = [newUser objectForKey: @"c_uid"];
|
|
if (key && ![key isEqualToString: login])
|
|
[[SOGoCache sharedCache]
|
|
setUserAttributes: [newUser jsonRepresentation]
|
|
forLogin: key];
|
|
|
|
emails = [[newUser objectForKey: @"emails"] objectEnumerator];
|
|
while ((key = [emails nextObject]))
|
|
[[SOGoCache sharedCache]
|
|
setUserAttributes: [newUser jsonRepresentation]
|
|
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;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @see [SOGoUser initWithLogin:roles:trust:]
|
|
*/
|
|
- (NSDictionary *) contactInfosForUserWithUIDorEmail: (NSString *) uid
|
|
{
|
|
NSRange r;
|
|
NSString *username, *domain;
|
|
NSDictionary *infos;
|
|
SOGoSystemDefaults *sd;
|
|
|
|
domain = nil;
|
|
infos = nil;
|
|
|
|
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
if ([sd enableDomainBasedUID])
|
|
{
|
|
r = [uid rangeOfString: @"@" options: NSBackwardsSearch];
|
|
if (r.location != NSNotFound)
|
|
{
|
|
// The domain is probably appended to the username;
|
|
// make sure it is a defined domain in the configuration.
|
|
domain = [uid substringFromIndex: (r.location + r.length)];
|
|
if ([[sd domainIds] containsObject: domain])
|
|
username = [uid substringToIndex: r.location];
|
|
else
|
|
domain = nil;
|
|
}
|
|
if (domain != nil)
|
|
infos = [self contactInfosForUserWithUIDorEmail: username
|
|
inDomain: domain];
|
|
}
|
|
|
|
if (infos == nil)
|
|
// If the user was not found using the domain or if no domain was detected,
|
|
// search using the original uid.
|
|
infos = [self contactInfosForUserWithUIDorEmail: uid
|
|
inDomain: nil];
|
|
|
|
return infos;
|
|
}
|
|
|
|
- (NSDictionary *) contactInfosForUserWithUIDorEmail: (NSString *) uid
|
|
inDomain: (NSString *) domain
|
|
{
|
|
NSMutableDictionary *currentUser;
|
|
NSString *aUID, *cacheUid, *jsonUser;
|
|
BOOL newUser;
|
|
|
|
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;
|
|
if (domain)
|
|
cacheUid = [NSString stringWithFormat: @"%@@%@", aUID, domain];
|
|
else
|
|
cacheUid = aUID;
|
|
jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: cacheUid];
|
|
currentUser = [jsonUser objectFromJSONString];
|
|
if ([currentUser isKindOfClass: NSNullK])
|
|
currentUser = nil;
|
|
else if (!([currentUser objectForKey: @"emails"]
|
|
&& [currentUser objectForKey: @"cn"]))
|
|
{
|
|
// We make sure that we either have no occurence of a cache entry or
|
|
// that we have an occurence with only a cached password. In the
|
|
// latter case, we update the entry with the remaining information
|
|
// and recache the value.
|
|
if (!currentUser || ([currentUser count] == 1 && [currentUser objectForKey: @"password"]))
|
|
{
|
|
newUser = YES;
|
|
|
|
if (!currentUser)
|
|
currentUser = [NSMutableDictionary dictionary];
|
|
}
|
|
else
|
|
newUser = NO;
|
|
[self _fillContactInfosForUser: currentUser
|
|
withUIDorEmail: aUID
|
|
inDomain: domain];
|
|
if (newUser)
|
|
{
|
|
if ([[currentUser objectForKey: @"c_uid"] length] == 0)
|
|
{
|
|
[self _retainUser: (NSDictionary *) [NSNull null]
|
|
withLogin: cacheUid];
|
|
currentUser = nil;
|
|
}
|
|
else
|
|
[self _retainUser: currentUser
|
|
withLogin: cacheUid];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
currentUser = nil;
|
|
|
|
return currentUser;
|
|
}
|
|
|
|
/**
|
|
* Fetch the contact information identified by the specified UID in the global addressbooks.
|
|
*/
|
|
- (NSDictionary *) fetchContactWithUID: (NSString *) uid
|
|
inDomain: (NSString *) domain
|
|
{
|
|
NSDictionary *contact;
|
|
NSMutableArray *contacts;
|
|
NSEnumerator *sources;
|
|
NSString *sourceID;
|
|
id currentSource;
|
|
|
|
contacts = [NSMutableArray array];
|
|
contact = nil;
|
|
sources = [[self addressBookSourceIDsInDomain: domain] objectEnumerator];
|
|
while ((sourceID = [sources nextObject]))
|
|
{
|
|
currentSource = [_sources objectForKey: sourceID];
|
|
contact = [currentSource lookupContactEntry: uid];
|
|
if (contact)
|
|
[contacts addObject: contact];
|
|
}
|
|
|
|
if ([contacts count])
|
|
contact = [[self _compactAndCompleteContacts: [contacts objectEnumerator]] lastObject];
|
|
else
|
|
contact = nil;
|
|
|
|
return contact;
|
|
}
|
|
|
|
- (NSArray *) _compactAndCompleteContacts: (NSEnumerator *) contacts
|
|
{
|
|
NSMutableDictionary *compactContacts, *returnContact;
|
|
NSDictionary *userEntry;
|
|
NSArray *newContacts, *allEmails;
|
|
NSMutableArray *emails;
|
|
NSString *uid, *email, *info;
|
|
NSNumber *isGroup;
|
|
id <SOGoSource> source;
|
|
NSUInteger count, max;
|
|
|
|
compactContacts = [NSMutableDictionary dictionary];
|
|
while ((userEntry = [contacts nextObject]))
|
|
{
|
|
uid = [userEntry objectForKey: @"c_uid"];
|
|
if ([uid length])
|
|
{
|
|
returnContact = [compactContacts objectForKey: uid];
|
|
if (!returnContact)
|
|
{
|
|
returnContact = [NSMutableDictionary dictionary];
|
|
[returnContact setObject: uid forKey: @"c_uid"];
|
|
source = [userEntry objectForKey: @"source"];
|
|
if (source)
|
|
[returnContact setObject: source forKey: @"source"];
|
|
[compactContacts setObject: returnContact forKey: uid];
|
|
}
|
|
if (![[returnContact objectForKey: @"c_name"] length])
|
|
[returnContact setObject: [userEntry objectForKey: @"c_name"]
|
|
forKey: @"c_name"];
|
|
if (![[returnContact objectForKey: @"cn"] length])
|
|
[returnContact setObject: [userEntry objectForKey: @"c_cn"]
|
|
forKey: @"cn"];
|
|
emails = [returnContact objectForKey: @"emails"];
|
|
if (!emails)
|
|
{
|
|
emails = [NSMutableArray array];
|
|
[returnContact setObject: emails forKey: @"emails"];
|
|
}
|
|
email = [userEntry objectForKey: @"mail"];
|
|
if ([email isKindOfClass: [NSArray class]])
|
|
{
|
|
allEmails = (NSArray *) email;
|
|
max = [allEmails count];
|
|
for (count = 0; count < max; count++)
|
|
{
|
|
email = [allEmails objectAtIndex: count];
|
|
[emails addObjectUniquely: email];
|
|
}
|
|
}
|
|
else if (email && ![emails containsObject: email])
|
|
[emails addObject: email];
|
|
email = [userEntry objectForKey: @"mozillasecondemail"];
|
|
if (email && ![emails containsObject: email])
|
|
[emails addObject: email];
|
|
email = [userEntry objectForKey: @"xmozillasecondemail"];
|
|
if (email && ![emails containsObject: email])
|
|
[emails addObject: email];
|
|
info = [userEntry objectForKey: @"c_info"];
|
|
if ([info length] > 0
|
|
&& ![[returnContact objectForKey: @"c_info"] length])
|
|
[returnContact setObject: info forKey: @"c_info"];
|
|
[self _fillContactMailRecords: returnContact];
|
|
isGroup = [userEntry objectForKey: @"isGroup"];
|
|
if (isGroup)
|
|
[returnContact setObject: isGroup forKey: @"isGroup"];
|
|
}
|
|
}
|
|
|
|
newContacts = [compactContacts allValues];
|
|
|
|
return newContacts;
|
|
}
|
|
|
|
- (NSArray *) _fetchEntriesInSources: (NSArray *) sourcesList
|
|
matching: (NSString *) filter
|
|
inDomain: (NSString *) domain
|
|
{
|
|
NSMutableArray *contacts;
|
|
NSEnumerator *sources;
|
|
NSString *sourceID;
|
|
id currentSource;
|
|
|
|
contacts = [NSMutableArray array];
|
|
sources = [sourcesList objectEnumerator];
|
|
while ((sourceID = [sources nextObject]))
|
|
{
|
|
currentSource = [_sources objectForKey: sourceID];
|
|
[contacts addObjectsFromArray:
|
|
[currentSource fetchContactsMatching: filter
|
|
inDomain: domain]];
|
|
}
|
|
|
|
return [self _compactAndCompleteContacts: [contacts objectEnumerator]];
|
|
}
|
|
|
|
- (NSArray *) fetchContactsMatching: (NSString *) filter
|
|
inDomain: (NSString *) domain
|
|
{
|
|
return [self
|
|
_fetchEntriesInSources: [self addressBookSourceIDsInDomain: domain]
|
|
matching: filter
|
|
inDomain: domain];
|
|
}
|
|
|
|
- (NSArray *) fetchUsersMatching: (NSString *) filter
|
|
inDomain: (NSString *) domain
|
|
{
|
|
return [self
|
|
_fetchEntriesInSources: [self authenticationSourceIDsInDomain: domain]
|
|
matching: filter
|
|
inDomain: domain];
|
|
}
|
|
|
|
- (NSString *) getLoginForDN: (NSString *) theDN
|
|
{
|
|
NSEnumerator *sources;
|
|
NSString *login;
|
|
NSObject <SOGoDNSource> *currentSource;
|
|
|
|
login = nil;
|
|
|
|
sources = [[_sources allValues] objectEnumerator];
|
|
while (!login && (currentSource = [sources nextObject]))
|
|
if ([currentSource conformsToProtocol: @protocol (SOGoDNSource)]
|
|
&& [theDN hasSuffix: [currentSource baseDN]])
|
|
login = [currentSource lookupLoginByDN: theDN];
|
|
|
|
return login;
|
|
}
|
|
|
|
@end
|