From a2129f3e4a8223fd2ebfdb97564d0aaedd39bc0d Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Thu, 16 Nov 2017 21:33:29 -0500 Subject: [PATCH] ModulesConstraints and listRequiresDot for SQL SQL sources used for authentication can now have module constraints. Entries of SQL sources used as address books can now be displayed automatically. --- Documentation/SOGoInstallationGuide.asciidoc | 19 +- NEWS | 2 + SoObjects/SOGo/LDAPSource.m | 3 +- SoObjects/SOGo/SQLSource.h | 6 +- SoObjects/SOGo/SQLSource.m | 246 +++++++++++-------- 5 files changed, 168 insertions(+), 108 deletions(-) diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 5ebd35956..6c3dd6db1 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -1612,7 +1612,7 @@ SQL source: [cols="3,47a,50"] |======================================================================= -.18+^|D |SOGoUserSources +.20+^|D |SOGoUserSources |Parameter used to set the SQL and/or LDAP sources used for authentication and global address books. Multiple sources can be specified as an array of dictionaries. A dictionary that defines a SQL @@ -1719,6 +1719,23 @@ to the user. See the _Multi-domains Configuration_ section in this document for more information. + +|listRequiresDot (optional) +|If set to `YES`, listing of this SQL source is only possible when performing a search (respecting the SOGoSearchMinimumWordLength parameter) or when explicitely typing a single dot. +Defaults to `YES` when unset. + +|ModulesConstraints (optional) +|Limits the access of any module through a constraint based on a SQL +column; must be a dictionary with keys `Mail`, and/or `Calendar`, +and/or `ActiveSync` for example: + +---- +ModulesConstraints = { + Calendar = { + c_ou = employees; + }; +}; +---- |======================================================================= Here is an example of an SQL-based authentication and address book diff --git a/NEWS b/NEWS index 19338d6c3..2c37970c7 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,8 @@ New features - [core] can now invite attendees to exceptions only (#2561) + - [core] add support for module constraints in SQL sources + - [core] add support for listRequiresDot in SQL sources - [web] display freebusy information of owner in appointment editor - [web] register SOGo as a handler for the mailto scheme (#1223) - [web] new events list view where events are grouped by day diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index 1ea076673..d5da0555c 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -229,8 +229,7 @@ static Class NSStringK; else queryTimeout = [dd ldapQueryTimeout]; - ASSIGN(modulesConstraints, - [udSource objectForKey: @"ModulesConstraints"]); + ASSIGN(modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]); ASSIGN(_filter, [udSource objectForKey: @"filter"]); ASSIGN(_userPasswordAlgorithm, [udSource objectForKey: @"userPasswordAlgorithm"]); ASSIGN(_scope, ([udSource objectForKey: @"scope"] diff --git a/SoObjects/SOGo/SQLSource.h b/SoObjects/SOGo/SQLSource.h index 572dd9b94..b3451df59 100644 --- a/SoObjects/SOGo/SQLSource.h +++ b/SoObjects/SOGo/SQLSource.h @@ -1,6 +1,6 @@ /* SQLSource.h - this file is part of SOGo * - * Copyright (C) 2009-2011 Inverse inc. + * Copyright (C) 2009-2017 Inverse inc. * * Authors: Ludovic Marcotte * Francis Lachapelle @@ -50,6 +50,10 @@ /* resources handling */ NSString *_kindField; NSString *_multipleBookingsField; + + BOOL _listRequiresDot; + + NSDictionary *_modulesConstraints; } @end diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index 39a66cc5c..e023c26d6 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -57,7 +57,7 @@ * * A SQL source can be defined like this: * - * { + * { * id = zot; * type = sql; * viewURL = "mysql://sogo:sogo@127.0.0.1:5432/sogo/sogo_view"; @@ -96,6 +96,8 @@ _multipleBookingsField = nil; _imapHostField = nil; _sieveHostField = nil; + _listRequiresDot = YES; + _modulesConstraints = nil; } return self; @@ -114,13 +116,16 @@ [_domainField release]; [_imapHostField release]; [_sieveHostField release]; - + [_modulesConstraints release]; + [super dealloc]; } - (id) initFromUDSource: (NSDictionary *) udSource inDomain: (NSString *) sourceDomain { + NSNumber *dotValue; + self = [self init]; ASSIGN(_sourceID, [udSource objectForKey: @"id"]); @@ -134,17 +139,22 @@ ASSIGN(_kindField, [udSource objectForKey: @"KindFieldName"]); ASSIGN(_multipleBookingsField, [udSource objectForKey: @"MultipleBookingsFieldName"]); ASSIGN(_domainField, [udSource objectForKey: @"DomainFieldName"]); + ASSIGN(_modulesConstraints, [udSource objectForKey: @"ModulesConstraints"]); if ([udSource objectForKey: @"prependPasswordScheme"]) _prependPasswordScheme = [[udSource objectForKey: @"prependPasswordScheme"] boolValue]; else _prependPasswordScheme = NO; - + if (!_userPasswordAlgorithm) _userPasswordAlgorithm = @"none"; if ([udSource objectForKey: @"viewURL"]) _viewURL = [[NSURL alloc] initWithString: [udSource objectForKey: @"viewURL"]]; + dotValue = [udSource objectForKey: @"listRequiresDot"]; + if (dotValue) + [self setListRequiresDot: [dotValue boolValue]]; + #warning this domain code has no effect yet if ([sourceDomain length]) ASSIGN (_domain, sourceDomain); @@ -183,7 +193,7 @@ { NSString *pass; NSString* result; - + pass = [plainPassword asCryptedPassUsingScheme: _userPasswordAlgorithm]; if (pass == nil) @@ -264,7 +274,7 @@ @" WHERE ", [_viewURL gcsTableName]]; if (_authenticationFilter) - { + { qualifier = [[EOAndQualifier alloc] initWithQualifiers: qualifier, [EOQualifier qualifierWithQualifierFormat: _authenticationFilter], @@ -272,18 +282,18 @@ [qualifier autorelease]; } [qualifier _gcsAppendToString: sql]; - + ex = [channel evaluateExpressionX: sql]; if (!ex) { NSDictionary *row; NSArray *attrs; NSString *value; - + attrs = [channel describeResults: NO]; row = [channel fetchAttributes: attrs withZone: NULL]; value = [row objectForKey: @"c_password"]; - + rc = [self _isPassword: _pwd equalTo: value]; [channel cancelFetch]; } @@ -308,9 +318,9 @@ * @return YES if the password was successfully changed. */ - (BOOL) changePasswordForLogin: (NSString *) login - oldPassword: (NSString *) oldPassword - newPassword: (NSString *) newPassword - perr: (SOGoPasswordPolicyError *) perr + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr { EOAdaptorChannel *channel; GCSChannelManager *cm; @@ -339,10 +349,10 @@ if (channel) { sqlstr = [NSString stringWithFormat: (@"UPDATE %@" - @" SET c_password = '%@'" - @" WHERE c_uid = '%@'"), - [_viewURL gcsTableName], encryptedPassword, login]; - + @" SET c_password = '%@'" + @" WHERE c_uid = '%@'"), + [_viewURL gcsTableName], encryptedPassword, login]; + ex = [channel evaluateExpressionX: sqlstr]; if (!ex) { @@ -355,13 +365,13 @@ [cm releaseChannel: channel]; } } - + return didChange; } - (NSString *) _whereClauseFromArray: (NSArray *) theArray value: (NSString *) theValue - exact: (BOOL) theBOOL + exact: (BOOL) theBOOL { NSMutableString *s; int i; @@ -379,6 +389,35 @@ return s; } +- (void) _fillConstraintsForModule: (NSString *) module + intoRecord: (NSMutableDictionary *) record +{ + NSDictionary *constraints; + NSEnumerator *matches; + NSString *currentMatch, *currentValue, *recordValue; + BOOL result; + + result = YES; + + constraints = [_modulesConstraints objectForKey: module]; + if (constraints) + { + matches = [[constraints allKeys] objectEnumerator]; + while (result == YES && (currentMatch = [matches nextObject])) + { + currentValue = [constraints objectForKey: currentMatch]; + recordValue = [record objectForKey: currentMatch]; + result = NO; + + if ([recordValue caseInsensitiveMatches: currentValue]) + result = YES; + } + } + + [record setObject: [NSNumber numberWithBool: result] + forKey: [NSString stringWithFormat: @"%@Access", module]]; +} + - (NSDictionary *) _lookupContactEntry: (NSString *) theID considerEmail: (BOOL) b inDomain: (NSString *) domain @@ -425,7 +464,7 @@ } } } - + domainQualifier = nil; if (_domainField && domain) { @@ -443,7 +482,7 @@ value: [theID lowercaseString]]; [loginQualifier autorelease]; [qualifiers addObject: loginQualifier]; - + if (_mailFields) { for (i = 0; i < [_mailFields count]; i++) @@ -461,7 +500,7 @@ } } } - + sql = [NSMutableString stringWithFormat: @"SELECT *" @" FROM %@" @" WHERE ", @@ -491,12 +530,9 @@ forKey: [field substringFromIndex: 2]]; } - // FIXME - // We have to do this here since we do not manage modules - // constraints right now over a SQL backend. - [response setObject: [NSNumber numberWithBool: YES] forKey: @"CalendarAccess"]; - [response setObject: [NSNumber numberWithBool: YES] forKey: @"MailAccess"]; - [response setObject: [NSNumber numberWithBool: YES] forKey: @"ActiveSyncAccess"]; + [self _fillConstraintsForModule: @"Calendar" intoRecord: response]; + [self _fillConstraintsForModule: @"Mail" intoRecord: response]; + [self _fillConstraintsForModule: @"ActiveSync" intoRecord: response]; // We set the domain, if any value = nil; @@ -510,7 +546,7 @@ // We populate all mail fields emails = [NSMutableArray array]; - + if ([response objectForKey: @"mail"]) [emails addObject: [response objectForKey: @"mail"]]; @@ -520,12 +556,12 @@ int i; for (i = 0; i < [_mailFields count]; i++) - if ((s = [response objectForKey: [_mailFields objectAtIndex: i]]) && + if ((s = [response objectForKey: [_mailFields objectAtIndex: i]]) && [s isNotNull] && - [[s stringByTrimmingSpaces] length] > 0) - [emails addObjectsFromArray: [s componentsSeparatedByString: @" "]]; + [[s stringByTrimmingSpaces] length] > 0) + [emails addObjectsFromArray: [s componentsSeparatedByString: @" "]]; } - + [response setObject: emails forKey: @"c_emails"]; if (_imapHostField) { @@ -576,7 +612,7 @@ } else [response setObject: [NSNumber numberWithBool: YES] forKey: @"canAuthenticate"]; - + // We check if we should use a different login for IMAP if (_imapLoginField) { @@ -586,26 +622,26 @@ // We check if it's a resource of not if (_kindField) - { + { if ((value = [response objectForKey: _kindField]) && [value isNotNull]) - { - if ([value caseInsensitiveCompare: @"location"] == NSOrderedSame || - [value caseInsensitiveCompare: @"thing"] == NSOrderedSame || - [value caseInsensitiveCompare: @"group"] == NSOrderedSame) - { - [response setObject: [NSNumber numberWithInt: 1] - forKey: @"isResource"]; - } - } + { + if ([value caseInsensitiveCompare: @"location"] == NSOrderedSame || + [value caseInsensitiveCompare: @"thing"] == NSOrderedSame || + [value caseInsensitiveCompare: @"group"] == NSOrderedSame) + { + [response setObject: [NSNumber numberWithInt: 1] + forKey: @"isResource"]; + } + } } if (_multipleBookingsField) { if ((value = [response objectForKey: _multipleBookingsField])) - { - [response setObject: [NSNumber numberWithInt: [value intValue]] - forKey: @"numberOfSimultaneousBookings"]; - } + { + [response setObject: [NSNumber numberWithInt: [value intValue]] + forKey: @"numberOfSimultaneousBookings"]; + } } [response setObject: self forKey: @"source"]; @@ -730,7 +766,7 @@ NSString *value; attrs = [channel describeResults: NO]; - + while ((row = [channel fetchAttributes: attrs withZone: NULL])) { value = [row objectForKey: @"c_uid"]; @@ -746,7 +782,7 @@ [self errorWithFormat:@"failed to acquire channel for URL: %@", [_viewURL absoluteString]]; - + return results; } @@ -764,73 +800,76 @@ NSException *ex; NSMutableString *sql; NSString *lowerFilter; - + results = [NSMutableArray array]; - cm = [GCSChannelManager defaultChannelManager]; - channel = [cm acquireOpenChannelForURL: _viewURL]; - if (channel) + if ([filter length] > 0 || !_listRequiresDot) { - lowerFilter = [filter lowercaseString]; - lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"]; - - sql = [NSMutableString stringWithFormat: (@"SELECT *" - @" FROM %@" - @" WHERE" - @" (LOWER(c_cn) LIKE '%%%@%%'" - @" OR LOWER(mail) LIKE '%%%@%%'"), - [_viewURL gcsTableName], - lowerFilter, lowerFilter]; - - if (_mailFields && [_mailFields count] > 0) + cm = [GCSChannelManager defaultChannelManager]; + channel = [cm acquireOpenChannelForURL: _viewURL]; + if (channel) { - [sql appendString: [self _whereClauseFromArray: _mailFields value: lowerFilter exact: NO]]; - } + lowerFilter = [filter lowercaseString]; + lowerFilter = [lowerFilter stringByReplacingString: @"'" withString: @"''"]; - [sql appendString: @")"]; + sql = [NSMutableString stringWithFormat: (@"SELECT *" + @" FROM %@" + @" WHERE" + @" (LOWER(c_cn) LIKE '%%%@%%'" + @" OR LOWER(mail) LIKE '%%%@%%'"), + [_viewURL gcsTableName], + lowerFilter, lowerFilter]; - if (_domainField) - { - if ([domain length]) + if (_mailFields && [_mailFields count] > 0) { - EOQualifier *domainQualifier; - domainQualifier = - [self _visibleDomainsQualifierFromDomain: domain]; - if (domainQualifier) + [sql appendString: [self _whereClauseFromArray: _mailFields value: lowerFilter exact: NO]]; + } + + [sql appendString: @")"]; + + if (_domainField) + { + if ([domain length]) { - [sql appendFormat: @" AND ("]; - [domainQualifier _gcsAppendToString: sql]; - [sql appendFormat: @")"]; + EOQualifier *domainQualifier; + domainQualifier = + [self _visibleDomainsQualifierFromDomain: domain]; + if (domainQualifier) + { + [sql appendFormat: @" AND ("]; + [domainQualifier _gcsAppendToString: sql]; + [sql appendFormat: @")"]; + } + } + else + [sql appendFormat: @" AND %@ IS NULL", _domainField]; + } + + ex = [channel evaluateExpressionX: sql]; + if (!ex) + { + NSDictionary *row; + NSArray *attrs; + + attrs = [channel describeResults: NO]; + + while ((row = [channel fetchAttributes: attrs withZone: NULL])) + { + row = [row mutableCopy]; + [(NSMutableDictionary *) row setObject: self forKey: @"source"]; + [results addObject: row]; + [row release]; } } else - [sql appendFormat: @" AND %@ IS NULL", _domainField]; - } - - ex = [channel evaluateExpressionX: sql]; - if (!ex) - { - NSDictionary *row; - NSArray *attrs; - - attrs = [channel describeResults: NO]; - - while ((row = [channel fetchAttributes: attrs withZone: NULL])) - { - row = [row mutableCopy]; - [(NSMutableDictionary *) row setObject: self forKey: @"source"]; - [results addObject: row]; - [row release]; - } + [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex]; + [cm releaseChannel: channel]; } else - [self errorWithFormat: @"could not run SQL '%@': %@", sql, ex]; - [cm releaseChannel: channel]; + [self errorWithFormat:@"failed to acquire channel for URL: %@", + [_viewURL absoluteString]]; } - else - [self errorWithFormat:@"failed to acquire channel for URL: %@", - [_viewURL absoluteString]]; - + return results; } @@ -856,13 +895,12 @@ - (void) setListRequiresDot: (BOOL) newListRequiresDot { + _listRequiresDot = newListRequiresDot; } - (BOOL) listRequiresDot { - /* This method is not implemented for SQLSource. It must enable a mechanism - where using "." is not required to list the content of addressbooks. */ - return YES; + return _listRequiresDot; } /* card editing */