9414df26c5
This method is used everywhere to try to retrieve the login of the user (and normally use the return value to [SOGoUser initwithLogin: ...]) In multidomain environments (with DomainLessLogin = false) there were several paths (mostly in SOGoAppointmentObject.m) that were trying to create SOGoUser objects with incorrect login: using only the uid part, not full email. Then like domain based uid was enabled, these users had DomainLessLogin set to true and further calls tried to authenticate only with the uid part (and they should not). This affects to several methods in: * ActiveSync/SOGoActiveSyncDispatcher.m * Appointments/SOGoAppointmentFolder.m * Appointments/SOGoAppointmentObject.m * Appointments/SOGoCalendarComponent.m * SOGoSAML2Session.m Probably a few features related with calendars are now fixed or working as intended in multidomain environments where the email is used as login
1146 lines
35 KiB
Objective-C
1146 lines
35 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;
|
|
SOGoSystemDefaults *sd;
|
|
|
|
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];
|
|
|
|
if ([dd forceExternalLoginWithEmail])
|
|
{
|
|
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
if ([sd enableDomainBasedUID])
|
|
// On multidomain environment we must use uid@domain
|
|
// for getEmailForUID method
|
|
login = [NSString stringWithFormat: @"%@@%@", uid, domain];
|
|
else
|
|
login = uid;
|
|
login = [self getEmailForUID: login];
|
|
}
|
|
else
|
|
login = uid;
|
|
}
|
|
|
|
return login;
|
|
}
|
|
|
|
- (NSString *) getUIDForEmail: (NSString *) email
|
|
{
|
|
NSDictionary *info;
|
|
SOGoSystemDefaults *sd;
|
|
NSString *uid, *domain;
|
|
|
|
info = [self contactInfosForUserWithUIDorEmail: email];
|
|
uid = [info objectForKey: @"c_uid"];
|
|
|
|
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
if ([sd enableDomainBasedUID]
|
|
&& ![[info objectForKey: @"DomainLessLogin"] boolValue])
|
|
{
|
|
domain = [info objectForKey: @"c_domain"];
|
|
uid = [NSString stringWithFormat: @"%@@%@", uid, domain];
|
|
}
|
|
|
|
return 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;
|
|
|
|
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;
|
|
NSDictionary *failedCount;
|
|
NSString *dictPassword, *username, *jsonUser;
|
|
SOGoSystemDefaults *dd;
|
|
BOOL checkOK;
|
|
|
|
dd = [SOGoSystemDefaults sharedSystemDefaults];
|
|
|
|
username = _login;
|
|
|
|
if (*_domain)
|
|
{
|
|
if ([_login rangeOfString: @"@"].location == NSNotFound)
|
|
username = [NSString stringWithFormat: @"%@@%@", _login, *_domain];
|
|
}
|
|
else
|
|
{
|
|
NSRange r;
|
|
|
|
// We try to extract the domain in use in order to avoid pounding all the authentication
|
|
// sources if SOGoLoginDomains isn't specified. This is also true if the user is
|
|
// using DAV or EAS.
|
|
r = [username rangeOfString: @"@"];
|
|
|
|
if (r.location != NSNotFound)
|
|
{
|
|
NSArray *allDomains;
|
|
int i;
|
|
|
|
*_domain = [username substringFromIndex: r.location+1];
|
|
|
|
allDomains = [[dd dictionaryForKey: @"domains"] allValues];
|
|
|
|
for (i = 0; i < [allDomains count]; i++)
|
|
{
|
|
if ([*_domain isEqualToString: [[allDomains objectAtIndex: i] objectForKey: @"SOGoMailDomain"]])
|
|
break;
|
|
}
|
|
|
|
// We haven't found one
|
|
if (i == [allDomains count])
|
|
*_domain = nil;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
failedCount = [[SOGoCache sharedCache] failedCountForLogin: username];
|
|
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];
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
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] &&
|
|
[login rangeOfString: @"@"].location == NSNotFound)
|
|
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;
|
|
NSEnumerator *enumerator;
|
|
NSString *access_type;
|
|
NSArray *access_types_list = [NSArray arrayWithObjects: @"CalendarAccess",
|
|
@"MailAccess",
|
|
@"ActiveSyncAccess",
|
|
nil];
|
|
|
|
emails = [NSMutableArray array];
|
|
cn = nil;
|
|
c_uid = nil;
|
|
c_domain = nil;
|
|
c_imaphostname = nil;
|
|
c_imaplogin = nil;
|
|
c_sievehostname = nil;
|
|
|
|
enumerator = [access_types_list objectEnumerator];
|
|
while ((access_type = [enumerator nextObject]) != nil)
|
|
[currentUser setObject: [NSNumber numberWithBool: YES]
|
|
forKey: access_type];
|
|
|
|
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"];
|
|
|
|
enumerator = [access_types_list objectEnumerator];
|
|
while ((access_type = [enumerator nextObject]) != nil)
|
|
{
|
|
access = [[userEntry objectForKey: access_type] boolValue];
|
|
if (!access)
|
|
[currentUser setObject: [NSNumber numberWithBool: NO]
|
|
forKey: access_type];
|
|
}
|
|
|
|
// 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, *user_json;
|
|
|
|
user_json = [newUser jsonRepresentation];
|
|
[[SOGoCache sharedCache] setUserAttributes: user_json
|
|
forLogin: login];
|
|
if (![newUser isKindOfClass: NSNullK])
|
|
{
|
|
emails = [[newUser objectForKey: @"emails"] objectEnumerator];
|
|
while ((key = [emails nextObject]))
|
|
{
|
|
if (![key isEqualToString: login])
|
|
[[SOGoCache sharedCache] setUserAttributes: user_json
|
|
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
|