From 2ef849ca653e0429e09c560381eb0b289415f5ab Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Mon, 6 Jun 2022 16:39:26 -0400 Subject: [PATCH] feat(preferences): password constraints for SQL sources Initial implementation of some password policy support for SQL sources. --- Documentation/SOGoInstallationGuide.asciidoc | 33 ++++++++++++++ SoObjects/SOGo/LDAPSource.m | 5 +++ SoObjects/SOGo/SOGoSource.h | 1 + SoObjects/SOGo/SQLSource.h | 1 + SoObjects/SOGo/SQLSource.m | 44 +++++++++++++++++-- UI/PreferencesUI/UIxPreferences.m | 12 +++++ UI/Templates/PreferencesUI/UIxPreferences.wox | 7 +++ 7 files changed, 99 insertions(+), 4 deletions(-) diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index d96b824d6..09a4f8b12 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -1660,6 +1660,39 @@ Other columns can exist and will actually be mapped automatically if they have the same name as popular LDAP attributes (such as `givenName`, `sn`, `department`, `title`, `telephoneNumber`, etc.). +|userPasswordPolicy + +|An array of dictionaries that define regular expressions used to determine whether a new password + is valid. + +Each dictionary must contain the key "regex" associated to a string representing a regular +expression. It can also contain the key "label" to briefly describe the constraint to the user. Example: + +---- +userPasswordPolicy = ( + { + label = "Minimum of 1 lowercase letter"; + regex = "[a-z]"; + }, + { + label = "Minimum of 1 uppercase letter"; + regex = "[A-Z]"; + }, + { + label = "Minimum of 1 digit"; + regex = "[0-9]"; + }, + { + label = "Minimum of 2 special symbols"; + regex = "([%$&*(){}!?\@#].*){2,}"; + }, + { + label = "Minimum length of 8 characters"; + regex = ".{8,}"; + } +); +---- + |userPasswordAlgorithm |The default algorithm used for password encryption when changing passwords. Possible values are: `none`, `plain`, `crypt`, `md5`, diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index 226ac76f6..f516292bf 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -405,6 +405,11 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses return _searchFields; } +- (NSArray *) userPasswordPolicy +{ + return nil; +} + - (void) setContactMapping: (NSDictionary *) theMapping andObjectClasses: (NSArray *) theObjectClasses { diff --git a/SoObjects/SOGo/SOGoSource.h b/SoObjects/SOGo/SOGoSource.h index 6d861a2b2..987ae5be8 100644 --- a/SoObjects/SOGo/SOGoSource.h +++ b/SoObjects/SOGo/SOGoSource.h @@ -40,6 +40,7 @@ - (NSString *) domain; - (NSArray *) searchFields; +- (NSArray *) userPasswordPolicy; - (id) connection; - (void) releaseConnection: (id) connection; diff --git a/SoObjects/SOGo/SQLSource.h b/SoObjects/SOGo/SQLSource.h index a9e957d8c..d44df1386 100644 --- a/SoObjects/SOGo/SQLSource.h +++ b/SoObjects/SOGo/SQLSource.h @@ -41,6 +41,7 @@ NSString *_imapLoginField; NSString *_imapHostField; NSString *_sieveHostField; + NSArray *_userPasswordPolicy; NSString *_userPasswordAlgorithm; NSString *_keyPath; NSURL *_viewURL; diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index d17d8f7a0..a6871c3ea 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -98,6 +98,7 @@ // "mail" expands to all entries of MailFieldNames _searchFields = [NSArray arrayWithObjects: @"c_cn", @"mail", nil]; [_searchFields retain]; + _userPasswordPolicy = nil; _userPasswordAlgorithm = nil; _keyPath = nil; _viewURL = nil; @@ -119,6 +120,7 @@ [_loginFields release]; [_mailFields release]; [_searchFields release]; + [_userPasswordPolicy release]; [_userPasswordAlgorithm release]; [_keyPath release]; [_viewURL release]; @@ -143,6 +145,7 @@ ASSIGN(_authenticationFilter, [udSource objectForKey: @"authenticationFilter"]); ASSIGN(_loginFields, [udSource objectForKey: @"LoginFieldNames"]); ASSIGN(_mailFields, [udSource objectForKey: @"MailFieldNames"]); + ASSIGN(_userPasswordPolicy, [udSource objectForKey: @"userPasswordPolicy"]); ASSIGN(_userPasswordAlgorithm, [udSource objectForKey: @"userPasswordAlgorithm"]); ASSIGN(_keyPath, [udSource objectForKey: @"keyPath"]); ASSIGN(_imapLoginField, [udSource objectForKey: @"IMAPLoginFieldName"]); @@ -192,6 +195,11 @@ return _searchFields; } +- (NSArray *) userPasswordPolicy +{ + return _userPasswordPolicy; +} + - (BOOL) _isPassword: (NSString *) plainPassword equalTo: (NSString *) encryptedPassword { @@ -328,7 +336,7 @@ * @param login the user's login name. * @param oldPassword the previous password. * @param newPassword the new password. - * @param perr is not used. + * @param perr will be set if the new password is not conform to the policy. * @return YES if the password was successfully changed. */ - (BOOL) changePasswordForLogin: (NSString *) login @@ -336,20 +344,48 @@ newPassword: (NSString *) newPassword perr: (SOGoPasswordPolicyError *) perr { + BOOL didChange, isOldPwdOk, isPolicyOk; EOAdaptorChannel *channel; GCSChannelManager *cm; + NSDictionary *policy; + NSEnumerator *policies; NSException *ex; - NSString *sqlstr; - BOOL didChange; - BOOL isOldPwdOk; + NSRange match; + NSString *sqlstr, *regex; + *perr = -1; isOldPwdOk = NO; + isPolicyOk = YES; didChange = NO; // Verify current password isOldPwdOk = [self checkLogin:login password:oldPassword perr:perr expire:0 grace:0]; if (isOldPwdOk) + { + if ([_userPasswordPolicy count]) + { + policies = [_userPasswordPolicy objectEnumerator]; + while (isPolicyOk && (policy = [policies nextObject])) + { + regex = [policy objectForKey: @"regex"]; + if (regex) + { + match = [newPassword rangeOfString: regex options: NSRegularExpressionSearch]; + isPolicyOk = isPolicyOk && match.length > 0; + if (match.length == 0) + { + // [self errorWithFormat: @"Password not conform to policy %@ (%@)", regex, [policy objectForKey: @"label"]]; + *perr = PolicyInsufficientPasswordQuality; + } + } + else + [self errorWithFormat: @"Invalid password policy (missing regex): %@", policy]; + } + } + } + + if (isOldPwdOk && isPolicyOk) { // Encrypt new password NSString *encryptedPassword = [self _encryptPassword: newPassword]; diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 5717191f8..439fe9922 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -38,6 +38,7 @@ #import #import #import +#import #import #import #import @@ -250,6 +251,17 @@ static NSArray *reminderValues = nil; return shortDateFormatText; } +// +// Used by wox template +// +- (NSArray *) passwordPolicy +{ + NSObject *userSource; + + userSource = [user authenticationSource]; + return [userSource userPasswordPolicy]; +} + // // Used internally // diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index 2a49dcfde..4eae4f120 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -282,6 +282,13 @@ +
+
    + +
  • +
    +
+