From 40c3cb74d0e08d5205302efa6bd16afe5a99d686 Mon Sep 17 00:00:00 2001 From: Ludovic Marcotte Date: Mon, 8 Mar 2010 15:18:05 +0000 Subject: [PATCH] See ChangeLog Monotone-Parent: 024380d579a482e49866c36ef54561cf8b39ab02 Monotone-Revision: 2cbc46c16f9b3d45d868b15a968f614dbbaf9749 Monotone-Author: ludovic@Sophos.ca Monotone-Date: 2010-03-08T15:18:05 Monotone-Branch: ca.inverse.sogo --- ChangeLog | 13 + SOPE/sope-patchset-r1664.diff | 350 +++++++++++++++++- SoObjects/SOGo/GNUmakefile | 2 + SoObjects/SOGo/LDAPSource.h | 5 +- SoObjects/SOGo/LDAPSource.m | 107 +++++- SoObjects/SOGo/SOGoAuthenticator.h | 2 +- SoObjects/SOGo/SOGoConstants.h | 48 +++ SoObjects/SOGo/SOGoConstants.m | 27 ++ SoObjects/SOGo/SOGoDAVAuthenticator.m | 19 +- SoObjects/SOGo/SOGoDomainDefaults.h | 4 +- SoObjects/SOGo/SOGoDomainDefaults.m | 13 +- SoObjects/SOGo/SOGoSource.h | 16 +- SoObjects/SOGo/SOGoSystemDefaults.h | 2 +- SoObjects/SOGo/SOGoUserManager.h | 15 +- SoObjects/SOGo/SOGoUserManager.m | 135 +++++-- SoObjects/SOGo/SOGoWebAuthenticator.h | 13 +- SoObjects/SOGo/SOGoWebAuthenticator.m | 33 +- SoObjects/SOGo/SQLSource.m | 29 +- UI/MainUI/SOGoRootPage.h | 2 +- UI/MainUI/SOGoRootPage.m | 84 ++++- UI/MainUI/product.plist | 5 + UI/Templates/PreferencesUI/UIxPreferences.wox | 38 +- .../JavascriptAPIExtensions.js | 5 +- UI/WebServerResources/UIxPreferences.css | 26 +- UI/WebServerResources/UIxPreferences.js | 136 ++++++- UI/WebServerResources/generic.js | 28 ++ 26 files changed, 1059 insertions(+), 98 deletions(-) create mode 100644 SoObjects/SOGo/SOGoConstants.h create mode 100644 SoObjects/SOGo/SOGoConstants.m diff --git a/ChangeLog b/ChangeLog index fde1aba5c..9ff2488d4 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +2010-03-08 Ludovic Marcotte + + * Added SoObjects/SOGo/SOGoConstants.{h,m} - a new file + that will hold eventually all SOGo constants. + + * Reworked the authentication code to use a generic + method for password authentication (LDAP + SQL). + + * Added password change support + + * Updated the SOPE patchset which now includes + password-policy support. + 2010-03-05 Wolfgang Sourdeau * SoObjects/SOGo/NSString+DAV.m (-davSetterName): enhanced so that diff --git a/SOPE/sope-patchset-r1664.diff b/SOPE/sope-patchset-r1664.diff index 9b257f838..350b69ca5 100644 --- a/SOPE/sope-patchset-r1664.diff +++ b/SOPE/sope-patchset-r1664.diff @@ -1,3 +1,311 @@ +Index: sope-ldap/NGLdap/NGLdapConnection.m +=================================================================== +--- sope-ldap/NGLdap/NGLdapConnection.m (revision 1664) ++++ sope-ldap/NGLdap/NGLdapConnection.m (working copy) +@@ -26,7 +26,6 @@ + #include "NGLdapModification.h" + #include "EOQualifier+LDAP.h" + #include "common.h" +-#include + + static BOOL LDAPDebugEnabled = NO; + static BOOL LDAPInitialBindSpecific = NO; +@@ -310,6 +309,295 @@ + return NO; + } + ++#ifdef LDAP_CONTROL_PASSWORDPOLICYREQUEST ++- (BOOL) bindWithMethod: (NSString *) _method ++ binddn: (NSString *) _login ++ credentials: (NSString *) _cred ++ perr: (LDAPPasswordPolicyError *) _perr ++ expire: (int *) _expire ++ grace: (int *) _grace ++{ ++ LDAPControl **sctrlsp = NULL; ++ LDAPControl *sctrls[2]; ++ LDAPControl sctrl[2]; ++ LDAPControl **ctrls; ++ LDAPControl c, *ctrl; ++ LDAPMessage *result = NULL; ++ ++ ++ int err, msgid, rc; ++ const char *l, *p; ++ char *matched = NULL; ++ char *info = NULL; ++ char **refs = NULL; ++ struct berval passwd = { 0, NULL }; ++ ++ l = (char *)[_login UTF8String]; ++ p = LDAPUseLatin1Creds ++ ? (char *)[_cred cString] ++ : (char *)[_cred UTF8String]; ++ ++ *_perr = -1; ++ passwd.bv_val = p; ++ passwd.bv_len = strlen(p); ++ ++ ++ c.ldctl_oid = LDAP_CONTROL_PASSWORDPOLICYREQUEST; ++ c.ldctl_value.bv_val = NULL; ++ c.ldctl_value.bv_len = 0; ++ c.ldctl_iscritical = 0; ++ sctrl[0] = c; ++ sctrls[0] = &sctrl[0]; ++ sctrls[1] = NULL; ++ ++ sctrlsp = sctrls; ++ ++ rc = ldap_sasl_bind(self->handle, l, LDAP_SASL_SIMPLE, &passwd, sctrlsp, NULL, &msgid); ++ ++ if (msgid == -1 || rc != LDAP_SUCCESS) ++ { ++ [self logWithFormat: @"bind - ldap_sasl_bind call failed"]; ++ return NO; ++ } ++ ++ rc = ldap_result(self->handle, msgid, LDAP_MSG_ALL, NULL, &result); ++ ++ if (rc == -1) ++ { ++ [self logWithFormat: @"bind - ldap_result call failed"]; ++ if (result) ldap_msgfree(result); ++ return NO; ++ } ++ ++ [self logWithFormat: @"bind - ldap_result call result: %d", rc]; ++ ++ rc = ldap_parse_result(self->handle, result, &err, &matched, &info, &refs, &ctrls, 1); ++ ++ if (rc != LDAP_SUCCESS) ++ { ++ [self logWithFormat: @"bind - ldap_parse_result call failed"]; ++ //if (result) ldap_msgfree(result); => causes a crash!? ++ if (matched) ber_memfree(matched); ++ if (info) ber_memfree(info); ++ if (refs) ber_memvfree((void **)refs); ++ return NO; ++ } ++ ++ if (err == LDAP_SUCCESS) ++ self->flags.isBound = YES; ++ else ++ self->flags.isBound = NO; ++ ++ // Even if we aren't bound to the server, we continue and we go get the ++ // policy control ++ if (ctrls) ++ { ++ ctrl = ldap_control_find(LDAP_CONTROL_PASSWORDPOLICYRESPONSE, ctrls, NULL); ++ if (ctrl) ++ { ++ rc = ldap_parse_passwordpolicy_control(self->handle, ctrl, _expire, _grace, _perr); ++ ++ if (rc == LDAP_SUCCESS) ++ { ++ [self logWithFormat: @"bind - policy values: %d %d %d - bound: %d", *_expire, *_grace, *_perr, self->flags.isBound]; ++ } ++ else ++ [self logWithFormat: @"bind - ldap_parse_passwordpolicy call failed"]; ++ } ++ else ++ [self logWithFormat: @"bind - ldap_control_find call failed"]; ++ ++ ldap_controls_free(ctrls); ++ } ++ else ++ { ++ [self logWithFormat: @"bind - ldap_parse_result - ctrls is NULL"]; ++ } ++ ++ return self->flags.isBound; ++} ++ ++// ++// No need to bind prior to calling this method. In fact, ++// if a bind() was issued prior calling this method, it ++// will fail. ++// ++- (BOOL) changePasswordAtDn: (NSString *) _dn ++ oldPassword: (NSString *) _oldPassword ++ newPassword: (NSString *) _newPassword ++ perr: (LDAPPasswordPolicyError *) _perr ++ ++{ ++ const char *user, *p; ++ int rc; ++ ++ *_perr = -1; ++ ++ user = (char *)[_dn UTF8String]; ++ p = LDAPUseLatin1Creds ? (char *)[_oldPassword cString] : (char *)[_oldPassword UTF8String]; ++ ++ if (!self->flags.isBound) ++ { ++ rc = ldap_simple_bind_s(self->handle, user, p); ++ ++ if (rc == LDAP_SUCCESS) ++ { ++ struct berval newpw = { 0, NULL }; ++ struct berval oldpw = { 0, NULL }; ++ struct berval bv = {0, NULL}; ++ struct berval *retdata = NULL; ++ ++ LDAPControl *sctrls[2]; ++ LDAPControl **ctrls; ++ LDAPControl sctrl[2]; ++ LDAPControl c, *ctrl; ++ LDAPMessage *result; ++ ++ BerElement *ber = NULL; ++ ++ char *matcheddn = NULL, *retoid = NULL, *text = NULL, **refs = NULL; ++ int idd, grace, expire, code; ++ ++ self->flags.isBound = YES; ++ code = LDAP_OTHER; ++ ++ newpw.bv_val = LDAPUseLatin1Creds ? (char *)[_newPassword cString] : (char *)[_newPassword UTF8String]; ++ newpw.bv_len = strlen(newpw.bv_val); ++ ++ oldpw.bv_val = p; ++ oldpw.bv_len = strlen(p); ++ ++ ber = ber_alloc_t(LBER_USE_DER); ++ ++ if (ber == NULL) ++ return NO; ++ ++ ber_printf(ber, "{" /*}*/ ); ++ ber_printf(ber, "ts", LDAP_TAG_EXOP_MODIFY_PASSWD_ID, user); ++ ber_printf(ber, "tO", LDAP_TAG_EXOP_MODIFY_PASSWD_OLD, &oldpw); ++ ber_printf(ber, "tO", LDAP_TAG_EXOP_MODIFY_PASSWD_NEW, &newpw); ++ ber_printf(ber, /*{*/ "N}" ); ++ ++ rc = ber_flatten2(ber, &bv, 0 ); ++ ++ if (rc < 0) ++ { ++ [self logWithFormat: @"change password - ber_flatten2 call failed"]; ++ ber_free(ber, 1); ++ return NO; ++ } ++ ++ // Everything is alright... ++ *_perr = -1; ++ ++ c.ldctl_oid = LDAP_CONTROL_PASSWORDPOLICYREQUEST; ++ c.ldctl_value.bv_val = NULL; ++ c.ldctl_value.bv_len = 0; ++ c.ldctl_iscritical = 0; ++ sctrl[0] = c; ++ sctrls[0] = &sctrl[0]; ++ sctrls[1] = NULL; ++ ++ rc = ldap_set_option(self->handle, LDAP_OPT_SERVER_CONTROLS, sctrls); ++ ++ if (rc != LDAP_OPT_SUCCESS) ++ { ++ [self logWithFormat: @"change password - ldap_set_option call failed"]; ++ ber_free(ber, 1); ++ return NO; ++ } ++ ++ rc = ldap_extended_operation(self->handle, ++ LDAP_EXOP_MODIFY_PASSWD, &bv, ++ NULL, NULL, &idd); ++ ++ ber_free(ber, 1); ++ ++ if (rc != LDAP_SUCCESS ) ++ { ++ [self logWithFormat: @"change password - ldap_extended_operation call failed"]; ++ return NO; ++ } ++ ++ rc = ldap_result(self->handle, LDAP_RES_ANY, LDAP_MSG_ALL, NULL, &result); ++ ++ if (rc < 0) ++ { ++ [self logWithFormat: @"change password - ldap_result call failed"]; ++ return NO; ++ } ++ ++ rc = ldap_parse_result(self->handle, result, &code, &matcheddn, &text, &refs, &ctrls, 0 ); ++ ++ if (rc != LDAP_SUCCESS) ++ { ++ [self logWithFormat: @"change password - ldap_parse_result call failed, rc = %d, code = %d, matcheddn = %s, text = %s", rc, code, matcheddn, text]; ++ ber_memfree(text); ++ ber_memfree(matcheddn); ++ ber_memvfree((void **) refs); ++ free(ctrls); ++ return NO; ++ } ++ ++ rc = ldap_parse_extended_result(self->handle, result, &retoid, &retdata, 1); ++ if (rc != LDAP_SUCCESS) ++ { ++ [self logWithFormat: @"change password - ldap_parse_extended result call failed"]; ++ ber_memfree(text); ++ ber_memfree(matcheddn); ++ ber_memvfree((void **) refs); ++ ber_memfree(retoid); ++ ber_bvfree(retdata); ++ free(ctrls); ++ return NO; ++ } ++ ++ ctrl = ldap_control_find(LDAP_CONTROL_PASSWORDPOLICYRESPONSE, ctrls, NULL); ++ ++ if (ctrl) ++ { ++ rc = ldap_parse_passwordpolicy_control(self->handle, ctrl, &expire, &grace, _perr); ++ ++ if (rc == LDAP_SUCCESS && *_perr == PP_noError) ++ { ++ [self logWithFormat: @"change password - policy values: %d %d %d", expire, grace, *_perr]; ++ } ++ else ++ { ++ [self logWithFormat: @"change password - ldap_parse_passwordpolicy call failed or error during password change: %d", *_perr]; ++ ber_memfree(text); ++ ber_memfree(matcheddn); ++ ber_memvfree((void **) refs); ++ ber_memfree(retoid); ++ ber_bvfree(retdata); ++ free(ctrls); ++ return NO; ++ } ++ } ++ else ++ { ++ // Ending up here doesn't mean that things failed. It could simply be caused by the ++ // fact that the password change was a success but no policy control object ++ // could be found. ++ [self logWithFormat: @"change password - ldap_control_find call failed"]; ++ } ++ ++ ber_memfree(text); ++ ber_memfree(matcheddn); ++ ber_memvfree((void **) refs); ++ ber_memfree(retoid); ++ ber_bvfree(retdata); ++ free(ctrls); ++ ++ return YES; ++ } ++ } ++ ++ return NO; ++} ++ ++#endif ++ + /* running queries */ + + - (void)setQueryTimeLimit:(NSTimeInterval)_timeLimit { Index: sope-ldap/NGLdap/NGLdapEntry.m =================================================================== --- sope-ldap/NGLdap/NGLdapEntry.m (revision 1664) @@ -25,7 +333,12 @@ Index: sope-ldap/NGLdap/ChangeLog =================================================================== --- sope-ldap/NGLdap/ChangeLog (revision 1664) +++ sope-ldap/NGLdap/ChangeLog (working copy) -@@ -1,3 +1,8 @@ +@@ -1,3 +1,13 @@ ++2010-03-08 Ludovic Marcotte ++ ++ * Added password policy support when binding to the ++ LDAP server or when changing passwords. ++ +2009-08-13 Wolfgang Sourdeau + + * NGLdapEntry.m (-attributeWithName:): attribute names are now @@ -34,6 +347,41 @@ Index: sope-ldap/NGLdap/ChangeLog 2009-04-02 Wolfgang Sourdeau * NGLdapConnection.m (useSSL,startTLS): new method enabling +Index: sope-ldap/NGLdap/NGLdapConnection.h +=================================================================== +--- sope-ldap/NGLdap/NGLdapConnection.h (revision 1664) ++++ sope-ldap/NGLdap/NGLdapConnection.h (working copy) +@@ -25,6 +25,9 @@ + #import + #import + ++#define LDAP_DEPRECATED 1 ++#include ++ + @class NSString, NSArray, NSEnumerator; + @class EOQualifier; + @class NGLdapEntry; +@@ -65,6 +68,20 @@ + - (BOOL)bindWithMethod:(NSString *)_method + binddn:(NSString *)_login credentials:(NSString *)_cred; + ++#ifdef LDAP_CONTROL_PASSWORDPOLICYREQUEST ++- (BOOL) bindWithMethod: (NSString *) _method ++ binddn: (NSString *) _login ++ credentials: (NSString *) _cred ++ perr: (LDAPPasswordPolicyError *) _perr ++ expire: (int *) _expire ++ grace: (int *) _grace; ++ ++- (BOOL) changePasswordAtDn: (NSString *) _dn ++ oldPassword: (NSString *) _oldPassword ++ newPassword: (NSString *) _newPassword ++ perr: (LDAPPasswordPolicyError *) _perr; ++#endif ++ + /* query parameters */ + + - (void)setQueryTimeLimit:(NSTimeInterval)_timeLimit; Index: sope-gdl1/PostgreSQL/PostgreSQL72Channel.m =================================================================== --- sope-gdl1/PostgreSQL/PostgreSQL72Channel.m (revision 1664) diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index 441ac374e..ac5e0dcaa 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -19,6 +19,7 @@ SOGo_HEADER_FILES = \ SOGoProductLoader.h \ \ SOGoCache.h \ + SOGoConstants.h \ SOGoObject.h \ SOGoContentObject.h \ SOGoFolder.h \ @@ -80,6 +81,7 @@ SOGo_OBJC_FILES = \ SOGoProductLoader.m \ \ SOGoCache.m \ + SOGoConstants.m \ SOGoObject.m \ SOGoContentObject.m \ SOGoFolder.m \ diff --git a/SoObjects/SOGo/LDAPSource.h b/SoObjects/SOGo/LDAPSource.h index ab09c6661..4c3a418a1 100644 --- a/SoObjects/SOGo/LDAPSource.h +++ b/SoObjects/SOGo/LDAPSource.h @@ -1,6 +1,6 @@ /* LDAPSource.h - this file is part of SOGo * - * Copyright (C) 2007-2009 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -26,6 +26,7 @@ #import #include "SOGoSource.h" +#include "SOGoConstants.h" @class NSDictionary; @class NSString; @@ -60,6 +61,8 @@ NSDictionary *modulesConstraints; NSMutableArray *searchAttributes; + + BOOL passwordPolicy; } - (void) setBindDN: (NSString *) newBindDN diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index 958b9f032..89a9689ee 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -29,6 +29,7 @@ #import #import #import +#import #import "NSArray+Utilities.h" #import "NSString+Utilities.h" @@ -157,6 +158,7 @@ static NSArray *commonSearchFields; _filter = nil; searchAttributes = nil; + passwordPolicy = NO; } return self; @@ -239,6 +241,9 @@ static NSArray *commonSearchFields; ASSIGN (_scope, ([udSource objectForKey: @"scope"] ? [udSource objectForKey: @"scope"] : (id)@"sub")); + + if ([udSource objectForKey: @"passwordPolicy"]) + passwordPolicy = [[udSource objectForKey: @"passwordPolicy"] boolValue]; } return self; @@ -414,16 +419,19 @@ static NSArray *commonSearchFields; return userDN; } -- (BOOL) checkLogin: (NSString *) loginToCheck - andPassword: (NSString *) passwordToCheck +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace { - BOOL didBind; - NSString *userDN; NGLdapConnection *bindConnection; + NSString *userDN; + BOOL didBind; didBind = NO; - if ([loginToCheck length] > 0) + if ([_login length] > 0) { bindConnection = [[NGLdapConnection alloc] initWithHostName: hostname port: port]; @@ -432,16 +440,24 @@ static NSArray *commonSearchFields; if (queryTimeout > 0) [bindConnection setQueryTimeLimit: queryTimeout]; if (bindFields) - userDN = [self _fetchUserDNForLogin: loginToCheck]; + userDN = [self _fetchUserDNForLogin: _login]; else userDN = [NSString stringWithFormat: @"%@=%@,%@", - IDField, loginToCheck, baseDN]; + IDField, _login, baseDN]; if (userDN) { NS_DURING - didBind = [bindConnection bindWithMethod: @"simple" - binddn: userDN - credentials: passwordToCheck]; + if (!passwordPolicy) + didBind = [bindConnection bindWithMethod: @"simple" + binddn: userDN + credentials: _pwd]; + else + didBind = [bindConnection bindWithMethod: @"simple" + binddn: userDN + credentials: _pwd + perr: (void *)_perr + expire: _expire + grace: _grace]; NS_HANDLER ; NS_ENDHANDLER @@ -450,10 +466,79 @@ static NSArray *commonSearchFields; } [bindConnection release]; } - + return didBind; } +- (BOOL) changePasswordForLogin: (NSString *) login + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr + +{ + NGLdapConnection *bindConnection; + NSString *userDN; + BOOL didChange; + + didChange = NO; + + if ([login length] > 0) + { + bindConnection = [[NGLdapConnection alloc] initWithHostName: hostname + port: port]; + if (![encryption length] || [self _setupEncryption: bindConnection]) + { + if (queryTimeout > 0) + [bindConnection setQueryTimeLimit: queryTimeout]; + if (bindFields) + userDN = [self _fetchUserDNForLogin: login]; + else + userDN = [NSString stringWithFormat: @"%@=%@,%@", + IDField, login, baseDN]; + if (userDN) + { + NS_DURING + if (!passwordPolicy) + { + // We don't use a password policy - we simply use + // a modify-op to change the password + NGLdapModification *mod; + NGLdapAttribute *attr; + NSArray *changes; + + attr = [[NGLdapAttribute alloc] initWithAttributeName: @"userPassword"]; + [attr addStringValue: newPassword]; + + mod = [NGLdapModification replaceModification: attr]; + changes = [NSArray arrayWithObject: mod]; + perr = PolicyNoError; + + if ([bindConnection bindWithMethod: @"simple" + binddn: userDN + credentials: oldPassword]) + didChange = [bindConnection modifyEntryWithDN: userDN + changes: changes]; + else + didChange = NO; + } + else + didChange = [bindConnection changePasswordAtDn: userDN + oldPassword: oldPassword + newPassword: newPassword + perr: (void *)perr]; + NS_HANDLER + ; + NS_ENDHANDLER + ; + } + } + [bindConnection release]; + } + + return didChange; +} + + /* contact management */ - (EOQualifier *) _qualifierForFilter: (NSString *) filter { diff --git a/SoObjects/SOGo/SOGoAuthenticator.h b/SoObjects/SOGo/SOGoAuthenticator.h index 4272036e0..0b37c87fa 100644 --- a/SoObjects/SOGo/SOGoAuthenticator.h +++ b/SoObjects/SOGo/SOGoAuthenticator.h @@ -1,6 +1,6 @@ /* SOGoAuthenticator.h - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/SoObjects/SOGo/SOGoConstants.h b/SoObjects/SOGo/SOGoConstants.h new file mode 100644 index 000000000..c4fdfa8db --- /dev/null +++ b/SoObjects/SOGo/SOGoConstants.h @@ -0,0 +1,48 @@ +/* SOGoConstants.h - this file is part of SOGo + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Ludovic Marcotte + * + * 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 _SOGOCONSTANTS_H_ +#define _SOGOCONSTANTS_H_ + +// This is a perfect copy of the OpenLDAP's +// LDAPPasswordPolicyError enum. We redeclare it +// so that we always include the ppolicy code +// within SOGo. +typedef enum +{ + PolicyPasswordExpired = 0, + PolicyAccountLocked = 1, + PolicyChangeAfterReset = 2, + PolicyPasswordModNotAllowed = 3, + PolicyMustSupplyOldPassword = 4, + PolicyInsufficientPasswordQuality = 5, + PolicyPasswordTooShort = 6, + PolicyPasswordTooYoung = 7, + PolicyPasswordInHistory = 8, + PolicyNoError = 65535, +} SOGoPasswordPolicyError; + +// Domain defaults +extern NSString *SOGoPasswordChangeEnabled; +extern NSString *SOGoPasswordPolicyEnabled; + +#endif /* _SOGOCONSTANTS_H_ */ diff --git a/SoObjects/SOGo/SOGoConstants.m b/SoObjects/SOGo/SOGoConstants.m new file mode 100644 index 000000000..6e1b0202c --- /dev/null +++ b/SoObjects/SOGo/SOGoConstants.m @@ -0,0 +1,27 @@ +/* SOGoConstants.m - this file is part of SOGo + * + * Copyright (C) 2010 Inverse inc. + * + * Author: Ludovic Marcotte + * + * 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 + +// LDAP Password Policy Constants +NSString* SOGoPasswordChangeEnabled = @"SOGoPasswordChangeEnabled"; +NSString* SOGoPasswordPolicyEnabled = @"SOGoPasswordPolicyEnabled"; diff --git a/SoObjects/SOGo/SOGoDAVAuthenticator.m b/SoObjects/SOGo/SOGoDAVAuthenticator.m index ea7d0823e..34607f6fa 100644 --- a/SoObjects/SOGo/SOGoDAVAuthenticator.m +++ b/SoObjects/SOGo/SOGoDAVAuthenticator.m @@ -27,6 +27,7 @@ #import #import +#import "SOGoConstants.h" #import "SOGoUserManager.h" #import "SOGoPermissions.h" #import "SOGoUser.h" @@ -48,8 +49,22 @@ - (BOOL) checkLogin: (NSString *) _login password: (NSString *) _pwd { - return [[SOGoUserManager sharedUserManager] checkLogin: _login - andPassword: _pwd]; + SOGoPasswordPolicyError perr; + int expire, grace; + BOOL b; + + perr = PolicyNoError; + + b = [[SOGoUserManager sharedUserManager] checkLogin: _login + password: _pwd + perr: &perr + expire: &expire + grace: &grace]; + + if (b && perr == PolicyNoError) + return YES; + + return NO; } - (NSString *) passwordInContext: (WOContext *) context diff --git a/SoObjects/SOGo/SOGoDomainDefaults.h b/SoObjects/SOGo/SOGoDomainDefaults.h index f5281b18f..b07856960 100644 --- a/SoObjects/SOGo/SOGoDomainDefaults.h +++ b/SoObjects/SOGo/SOGoDomainDefaults.h @@ -1,6 +1,6 @@ /* SOGoDomainDefaults.h - this file is part of SOGo * - * Copyright (C) 2009 Inverse inc. + * Copyright (C) 2009-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -46,6 +46,8 @@ - (BOOL) sieveScriptsEnabled; - (BOOL) forwardEnabled; - (BOOL) vacationEnabled; +- (BOOL) passwordChangeEnabled; +- (BOOL) passwordPolicyEnabled; - (NSString *) mailingMechanism; - (NSString *) smtpServer; - (NSString *) mailSpoolPath; diff --git a/SoObjects/SOGo/SOGoDomainDefaults.m b/SoObjects/SOGo/SOGoDomainDefaults.m index 7cde35a26..52c9a22c0 100644 --- a/SoObjects/SOGo/SOGoDomainDefaults.m +++ b/SoObjects/SOGo/SOGoDomainDefaults.m @@ -1,6 +1,6 @@ /* SOGoDomainDefaults.m - this file is part of SOGo * - * Copyright (C) 2009 Inverse inc. + * Copyright (C) 2009-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -30,6 +30,7 @@ #import "SOGoSystemDefaults.h" #import "SOGoDomainDefaults.h" +#import "SOGoConstants.h" @implementation SOGoDomainDefaults @@ -170,6 +171,16 @@ return [self boolForKey: @"SOGoVacationEnabled"]; } +- (BOOL) passwordChangeEnabled +{ + return [self boolForKey: SOGoPasswordChangeEnabled]; +} + +- (BOOL) passwordPolicyEnabled +{ + return [self boolForKey: SOGoPasswordPolicyEnabled]; +} + - (NSString *) mailingMechanism { NSString *mailingMechanism; diff --git a/SoObjects/SOGo/SOGoSource.h b/SoObjects/SOGo/SOGoSource.h index 3c1495a07..18acc5093 100644 --- a/SoObjects/SOGo/SOGoSource.h +++ b/SoObjects/SOGo/SOGoSource.h @@ -1,6 +1,6 @@ /* SOGoSource.h - this file is part of SOGo * - * Copyright (C) 2009 Inverse inc. + * Copyright (C) 2009-2010 Inverse inc. * * Author: Ludovic Marcotte * @@ -25,6 +25,8 @@ #import +#import "SOGoConstants.h" + @class NSDictionary; @class NSString; @@ -38,8 +40,16 @@ - (NSString *) domain; -- (BOOL) checkLogin: (NSString *) login - andPassword: (NSString *) password; +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace; + +- (BOOL) changePasswordForLogin: (NSString *) login + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr; - (NSDictionary *) lookupContactEntry: (NSString *) theID; - (NSDictionary *) lookupContactEntryWithUIDorEmail: (NSString *) entryID; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index 2a166a158..1d0d6532b 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -1,6 +1,6 @@ /* SOGoSystemDefaults.h - this file is part of SOGo * - * Copyright (C) 2009 Inverse inc. + * Copyright (C) 2009-2010 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/SoObjects/SOGo/SOGoUserManager.h b/SoObjects/SOGo/SOGoUserManager.h index 7debfd96a..558a3bbd9 100644 --- a/SoObjects/SOGo/SOGoUserManager.h +++ b/SoObjects/SOGo/SOGoUserManager.h @@ -1,6 +1,6 @@ /* SOGoUserManager.h - this file is part of SOGo * - * Copyright (C) 2007-2009 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -25,6 +25,8 @@ #import +#import "SOGoConstants.h" + @class NSDictionary; @class NSMutableDictionary; @class NSString; @@ -75,9 +77,16 @@ - (NSString *) getUIDForEmail: (NSString *) email; - (NSString *) getLoginForDN: (NSString *) theDN; -- (BOOL) checkLogin: (NSString *) login - andPassword: (NSString *) password; +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace; +- (BOOL) changePasswordForLogin: (NSString *) login + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr; @end #endif /* SOGOUSERMANAGER_H */ diff --git a/SoObjects/SOGo/SOGoUserManager.m b/SoObjects/SOGo/SOGoUserManager.m index e38ca3063..9ade70d4f 100644 --- a/SoObjects/SOGo/SOGoUserManager.m +++ b/SoObjects/SOGo/SOGoUserManager.m @@ -1,6 +1,6 @@ /* SOGoUserManager.m - this file is part of SOGo * - * Copyright (C) 2007-2009 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -36,6 +36,7 @@ #import "SOGoSystemDefaults.h" #import "SOGoUserManager.h" #import "SOGoCache.h" +#import "SOGoConstants.h" #import "SOGoSource.h" @implementation SOGoUserManagerRegistry @@ -89,7 +90,7 @@ { NSString *sourceID, *value, *type; NSMutableDictionary *metadata; - NSObject *ldapSource; + NSObject *sogoSource; BOOL isAddressBook; Class c; @@ -97,10 +98,10 @@ if ([sourceID length] > 0) { type = [[udSource objectForKey: @"type"] lowercaseString]; - c = NSClassFromString ([_registry sourceClassForType: type]); - ldapSource = [c sourceFromUDSource: udSource inDomain: domain]; + c = NSClassFromString([_registry sourceClassForType: type]); + sogoSource = [c sourceFromUDSource: udSource inDomain: domain]; if (sourceID) - [_sources setObject: ldapSource forKey: sourceID]; + [_sources setObject: sogoSource forKey: sourceID]; else [self errorWithFormat: @"id field missing in an user source," @" check the SOGoUserSources defaults"]; @@ -335,45 +336,84 @@ { NSDictionary *contactInfos; -// NSLog (@"getUIDForEmail: %@", email); contactInfos = [self contactInfosForUserWithUIDorEmail: email]; return [contactInfos objectForKey: @"c_uid"]; } +- (BOOL) _sourceChangePasswordForLogin: (NSString *) login + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr +{ + NSObject *sogoSource; + NSEnumerator *authIDs; + NSString *currentID; + BOOL didChange; + + didChange = NO; + + authIDs = [[self authenticationSourceIDsInDomain: nil] 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 -{ - NSObject *ldapSource; + perr: (SOGoPasswordPolicyError *) perr + expire: (int *) expire + grace: (int *) grace +{ + NSObject *sogoSource; NSEnumerator *authIDs; NSString *currentID; BOOL checkOK; - + checkOK = NO; - + authIDs = [[self authenticationSourceIDsInDomain: nil] objectEnumerator]; while (!checkOK && (currentID = [authIDs nextObject])) { - ldapSource = [_sources objectForKey: currentID]; - checkOK = [ldapSource checkLogin: login andPassword: password]; + sogoSource = [_sources objectForKey: currentID]; + checkOK = [sogoSource checkLogin: login + password: password + perr: perr + expire: expire + grace: grace]; } + return checkOK; } -- (BOOL) checkLogin: (NSString *) login - andPassword: (NSString *) password +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace { - NSMutableDictionary *currentUser; NSString *dictPassword, *jsonUser; + NSMutableDictionary *currentUser; BOOL checkOK; - - jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: login]; + + jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: _login]; currentUser = [NSMutableDictionary dictionaryWithJSONString: jsonUser]; dictPassword = [currentUser objectForKey: @"password"]; if (currentUser && dictPassword) - checkOK = ([dictPassword isEqualToString: password]); - else if ([self _sourceCheckLogin: login andPassword: password]) + checkOK = ([dictPassword isEqualToString: _pwd]); + else if ([self _sourceCheckLogin: _login + andPassword: _pwd + perr: _perr + expire: _expire + grace: _grace]) { checkOK = YES; if (!currentUser) @@ -386,17 +426,58 @@ // 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: password forKey: @"password"]; + [currentUser setObject: _pwd forKey: @"password"]; [[SOGoCache sharedCache] setUserAttributes: [currentUser jsonStringValue] - forLogin: login]; + forLogin: _login]; } - else - checkOK = NO; + else + checkOK = NO; return checkOK; } +- (BOOL) changePasswordForLogin: (NSString *) login + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr +{ + NSString *dictPassword, *jsonUser; + NSMutableDictionary *currentUser; + BOOL didChange; + + jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: login]; + currentUser = [NSMutableDictionary dictionaryWithJSONString: jsonUser]; + dictPassword = [currentUser objectForKey: @"password"]; + + if ([self _sourceChangePasswordForLogin: login + 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 forKey: @"password"]; + [[SOGoCache sharedCache] + setUserAttributes: [currentUser jsonStringValue] + forLogin: login]; + } + else + didChange = NO; + + return didChange; +} + - (void) _fillContactMailRecords: (NSMutableDictionary *) contact { NSString *uid, *domain, *systemEmail; @@ -424,8 +505,8 @@ { NSMutableArray *emails; NSDictionary *userEntry; - NSEnumerator *ldapSources; - LDAPSource *currentSource; + NSEnumerator *sogoSources; + NSObject *currentSource; NSString *sourceID, *cn, *c_domain, *c_uid, *c_imaphostname; NSArray *c_emails; BOOL access; @@ -441,9 +522,9 @@ [currentUser setObject: [NSNumber numberWithBool: YES] forKey: @"MailAccess"]; - ldapSources = [[self authenticationSourceIDsInDomain: nil] + sogoSources = [[self authenticationSourceIDsInDomain: nil] objectEnumerator]; - while ((sourceID = [ldapSources nextObject])) + while ((sourceID = [sogoSources nextObject])) { currentSource = [_sources objectForKey: sourceID]; userEntry = [currentSource lookupContactEntryWithUIDorEmail: uid]; diff --git a/SoObjects/SOGo/SOGoWebAuthenticator.h b/SoObjects/SOGo/SOGoWebAuthenticator.h index 12a295c1c..804dd1650 100644 --- a/SoObjects/SOGo/SOGoWebAuthenticator.h +++ b/SoObjects/SOGo/SOGoWebAuthenticator.h @@ -1,8 +1,9 @@ /* SOGoWebAuthenticator.h - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau + * Ludovic Marcotte * * 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 @@ -24,7 +25,9 @@ #define _SOGOWEBAUTHENTICATOR_H_ #import + #import "SOGoAuthenticator.h" +#import "SOGoConstants.h" @class NSString; @@ -34,6 +37,12 @@ + (id) sharedSOGoWebAuthenticator; +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace; + @end -#endif /* _SOGOWEBAUTHENTICATOR_H__ */ +#endif /* _SOGOWEBAUTHENTICATOR_H_ */ diff --git a/SoObjects/SOGo/SOGoWebAuthenticator.m b/SoObjects/SOGo/SOGoWebAuthenticator.m index 7e49d6b93..634ab6889 100644 --- a/SoObjects/SOGo/SOGoWebAuthenticator.m +++ b/SoObjects/SOGo/SOGoWebAuthenticator.m @@ -1,6 +1,6 @@ /* SOGoWebAuthenticator.m - this file is part of SOGo * - * Copyright (C) 2007-2009 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * @@ -37,6 +37,7 @@ #import #import "SOGoCASSession.h" +#import "SOGoConstants.h" #import "SOGoPermissions.h" #import "SOGoSystemDefaults.h" #import "SOGoUser.h" @@ -59,11 +60,28 @@ - (BOOL) checkLogin: (NSString *) _login password: (NSString *) _pwd { + SOGoPasswordPolicyError perr; + int expire, grace; + + return [self checkLogin: _login + password: _pwd + perr: &perr + expire: &expire + grace: &grace]; +} + +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace +{ + SOGoCASSession *session; SOGoSystemDefaults *sd; BOOL rc; - SOGoCASSession *session; sd = [SOGoSystemDefaults sharedSystemDefaults]; + if ([[sd authenticationType] isEqualToString: @"cas"]) { session = [SOGoCASSession CASSessionWithIdentifier: _pwd]; @@ -74,8 +92,15 @@ } else rc = [[SOGoUserManager sharedUserManager] checkLogin: _login - andPassword: _pwd]; - + password: _pwd + perr: _perr + expire: _expire + grace: _grace]; + + [self logWithFormat: @"Checked login with ppolicy enabled: %d %d %d", *_perr, *_expire, *_grace]; + + // It's important to return the real value here. The callee will handle + // the return code and check for the _perr value. return rc; } diff --git a/SoObjects/SOGo/SQLSource.m b/SoObjects/SOGo/SQLSource.m index 39c52f796..81582b27a 100644 --- a/SoObjects/SOGo/SQLSource.m +++ b/SoObjects/SOGo/SQLSource.m @@ -1,6 +1,6 @@ /* SQLSource.h - this file is part of SOGo * - * Copyright (C) 2009 Inverse inc. + * Copyright (C) 2009-2010 Inverse inc. * * Author: Ludovic Marcotte * @@ -40,6 +40,8 @@ #include "SQLSource.h" +#include "SOGoConstants.h" + /** * The view MUST contain the following columns: * @@ -162,8 +164,17 @@ return NO; } -- (BOOL) checkLogin: (NSString *) login - andPassword: (NSString *) password +// +// SQL sources don't support right now all the password policy +// stuff supported by OpenLDAP (and others). If we want to support +// this for SQL sources, we'll have to implement the same +// kind of logic in this module. +// +- (BOOL) checkLogin: (NSString *) _login + password: (NSString *) _pwd + perr: (SOGoPasswordPolicyError *) _perr + expire: (int *) _expire + grace: (int *) _grace { EOAdaptorChannel *channel; GCSChannelManager *cm; @@ -180,7 +191,7 @@ sql = [NSString stringWithFormat: (@"SELECT c_password" @" FROM %@" @" WHERE c_uid = '%@'"), - [_viewURL gcsTableName], login]; + [_viewURL gcsTableName], _login]; ex = [channel evaluateExpressionX: sql]; if (!ex) @@ -193,7 +204,7 @@ row = [channel fetchAttributes: attrs withZone: NULL]; value = [row objectForKey: @"c_password"]; - rc = [self _isPassword: password equalTo: value]; + rc = [self _isPassword: _pwd equalTo: value]; [channel cancelFetch]; } else @@ -207,6 +218,14 @@ return rc; } +- (BOOL) changePasswordForLogin: (NSString *) login + oldPassword: (NSString *) oldPassword + newPassword: (NSString *) newPassword + perr: (SOGoPasswordPolicyError *) perr +{ + return NO; +} + - (NSDictionary *) _lookupContactEntry: (NSString *) theID considerEmail: (BOOL) b { diff --git a/UI/MainUI/SOGoRootPage.h b/UI/MainUI/SOGoRootPage.h index f52704717..6bbad00bf 100644 --- a/UI/MainUI/SOGoRootPage.h +++ b/UI/MainUI/SOGoRootPage.h @@ -1,6 +1,6 @@ /* SOGoRootPage.h - this file is part of SOGo * - * Copyright (C) 2007 Inverse inc. + * Copyright (C) 2007-2010 Inverse inc. * * Author: Wolfgang Sourdeau * diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index 4f0823898..1ebcb1e13 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -24,6 +24,7 @@ #import #import #import +#import #import #import @@ -39,13 +40,17 @@ #import #import +#import #import #import #import #import #import +#import #import +#import + #import "SOGoRootPage.h" @interface SOGoRootPage (crashAdditions) @@ -115,17 +120,37 @@ SOGoUserDefaults *ud; NSString *username, *password, *language; NSArray *supportedLanguages; - + + SOGoPasswordPolicyError err; + int expire, grace; + BOOL b; + + err = PolicyNoError; + expire = grace = 0; + auth = [[WOApplication application] - authenticatorInContext: context]; + authenticatorInContext: context]; request = [context request]; username = [request formValueForKey: @"userName"]; password = [request formValueForKey: @"password"]; language = [request formValueForKey: @"language"]; - if ([auth checkLogin: username password: password]) + + if ((b = [auth checkLogin: username + password: password + perr: &err + expire: &expire + grace: &grace]) + && (err == PolicyNoError || err == PolicyChangeAfterReset)) { [self logWithFormat: @"successful login for user '%@'", username]; response = [self responseWith204]; + + // We must warn the user that he has to change his password + if (err == PolicyChangeAfterReset) + { + + } + authCookie = [self _cookieWithUsername: username andPassword: password forAuthenticator: auth]; [response addCookie: authCookie]; @@ -141,7 +166,21 @@ } else { - [self logWithFormat: @"failed login for user '%@'", username]; + [self logWithFormat: @"Login for user '%@' might not have worked - password policy: %d bound: ", username, err, b]; + + if (err == PolicyNoError) + { + [self logWithFormat: @"failed login for user '%@' due to wrong password", username]; + } + else if (err == PolicyAccountLocked) + { + // Account has been locked due to too many failures + } + else if (err == PolicyPasswordExpired) + { + // The password MUST be changed - we need to ask for the old password and the new one here + } + response = [self responseWithStatus: 403]; } @@ -372,4 +411,41 @@ return aString; } +- (WOResponse *) changePasswordAction +{ + NSString *username, *password, *newPassword; + SOGoUserManager *um; + SOGoPasswordPolicyError error; + WOResponse *response; + WORequest *request; + NSDictionary *message, *jsonError; + + request = [context request]; + message = [NSMutableDictionary dictionaryWithJSONString: [request contentAsString]]; + username = [message objectForKey: @"userName"]; + password = [message objectForKey: @"password"]; + newPassword = [message objectForKey: @"newPassword"]; + + um = [SOGoUserManager sharedUserManager]; + + // This will also update the cached password in memcached. + if ([um changePasswordForLogin: username + oldPassword: password + newPassword: newPassword + perr: &error]) + response = [self responseWith204]; + else + { + jsonError + = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: error] + forKey: @"LDAPPasswordPolicyError"]; + response = [self responseWithStatus: 403 + andJSONRepresentation: jsonError]; + [response setHeader: @"application/json" + forKey: @"content-type"]; + } + + return response; +} + @end /* SOGoRootPage */ diff --git a/UI/MainUI/product.plist b/UI/MainUI/product.plist index af79789f0..578b3e109 100644 --- a/UI/MainUI/product.plist +++ b/UI/MainUI/product.plist @@ -130,6 +130,11 @@ pageName = "SOGoRootPage"; actionName = "connect"; }; + changePassword = { + protectedBy = ""; + pageName = "SOGoRootPage"; + actionName = "changePassword"; + }; GET = { // more or less a hack, see README of dbd protectedBy = ""; pageName = "SOGoRootPage"; diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index 0d28c129a..4a0bcaa7e 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -34,9 +34,9 @@ >
  • - - - +
    +
  • @@ -278,22 +278,22 @@
    - - - - - - - - - - - - - - - +
    +


    +
    + +

    +

    +
    +
    diff --git a/UI/WebServerResources/JavascriptAPIExtensions.js b/UI/WebServerResources/JavascriptAPIExtensions.js index fa7408733..55eef3c36 100644 --- a/UI/WebServerResources/JavascriptAPIExtensions.js +++ b/UI/WebServerResources/JavascriptAPIExtensions.js @@ -272,8 +272,7 @@ String.prototype.base64decode = function() { var enc1, enc2, enc3, enc4; var i = 0; - var input = this.replace(/[^A-Za-z0-9\+\/\=]/g, ""); - + var input = "" + this; // .replace(/[^A-Za-z0-9\+\/\=]/g, "") while (i < input.length) { enc1 = this._base64_keyStr.indexOf(input.charAt(i++)); enc2 = this._base64_keyStr.indexOf(input.charAt(i++)); @@ -294,7 +293,7 @@ String.prototype.base64decode = function() { } } - return output.utf8decode(); + return output; }; String.prototype.utf8encode = function() { diff --git a/UI/WebServerResources/UIxPreferences.css b/UI/WebServerResources/UIxPreferences.css index 6cd5fee86..170e226e4 100644 --- a/UI/WebServerResources/UIxPreferences.css +++ b/UI/WebServerResources/UIxPreferences.css @@ -5,10 +5,6 @@ DIV#preferencesTabs right: 5px; bottom: 5px; } -DIV#passwordView -{ padding-top: 3em; - padding-right: 10em; } - TEXTAREA#signature { position: absolute; width: 100%; @@ -107,3 +103,25 @@ TH#activeTableHeader TD.activeColumn { text-align: center; } + +P#passwordFields, +P#passwordError +{ position: absolute; + left: 0px; } + +#passwordFields +{ top: 40px; + width: 410px; + text-align: right; } + +P#passwordError +{ top: 92px; + width: 307px; + text-align: right; + margin-left: 40px; } + +P.errorMessage#passwordError +{ color: #f00; } + +P.infoMessage#passwordError +{ color: #00f; } diff --git a/UI/WebServerResources/UIxPreferences.js b/UI/WebServerResources/UIxPreferences.js index d6b27f357..087bbeaf7 100644 --- a/UI/WebServerResources/UIxPreferences.js +++ b/UI/WebServerResources/UIxPreferences.js @@ -105,7 +105,7 @@ function onChoiceChanged(event) { _setupEvents(false); } -function addDefaultEmailAddresses() { +function addDefaultEmailAddresses(event) { var defaultAddresses = $("defaultEmailAddresses").value.split(/, */); var addresses = $("autoReplyEmailAddresses").value.trim(); @@ -121,6 +121,8 @@ function addDefaultEmailAddresses() { }); $("autoReplyEmailAddresses").value = addresses.join(", "); + + event.stop(); } function initPreferences() { @@ -157,9 +159,13 @@ function initPreferences() { onReplyPlacementListChange (); } - if ($("addDefaultEmailAddresses")) - $("addDefaultEmailAddresses").observe("click", - addDefaultEmailAddresses); + var button = $("addDefaultEmailAddresses"); + if (button) + button.observe("click", addDefaultEmailAddresses); + + var button = $("changePasswordBtn"); + if (button) + button.observe("click", onChangePasswordClick); initSieveFilters(); } @@ -549,4 +555,126 @@ function onComposeMessagesTypeChange(event) { } } +function onChangePasswordClick(event) { + var field = $("newPasswordField"); + var confirmationField = $("newPasswordConfirmationField"); + if (field && confirmationField) { + var password = field.value; + if (password == confirmationField.value) { + if (password.length > 0) + changePassword(password); + else + showPasswordMessage(_("Password must not be empty."), + "error"); + } + else { + showPasswordMessage(_("The passwords do not match." + + " Please try again."), + "error"); + field.focus(); + field.select(); + } + } + event.stop(); +} + +/* TODO: this method could serve as a basis for a basic text container (for + example the log console. */ +function showPasswordMessage(message, msgType) { + var para = $("passwordError"); + if (para) { + if (!msgType) + msgType = "error"; + var typeClass = msgType + "Message"; + if (!para.typeClass || para.typeClass != typeClass) { + if (para.typeClass) { + para.removeClassName(para.typeClass); + } + para.typeClass = typeClass; + para.addClassName(typeClass); + } + if (!para.message || para.message != message) { + while (para.lastChild) { + para.removeChild(para.lastChild); + } + if (message) { + var sentences = message.split("\n"); + para.appendChild(document.createTextNode(sentences[0])); + for (var i = 1; i < sentences.length; i++) { + para.appendChild(document.createElement("br")); + para.appendChild(document.createTextNode(sentences[i])); + } + para.message = message; + } + } + } +} + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +function changePassword(newPassword) { + var loginValues = readLoginCookie(); + if (loginValues) { + var content = Object.toJSON({ userName: loginValues[0], + password: loginValues[1], + newPassword: newPassword }); + var url = ApplicationBaseURL + "../changePassword"; + triggerAjaxRequest(url, changePasswordCallback, loginValues[1], content, + {"content-type": "application/json"} ); + } +} + +function changePasswordCallback(http) { + if (http.readyState == 4) { + if (isHttpStatus204(http.status)) { + log("it worked"); + showPasswordMessage(_("The password was changed successfully."), "info"); + setLoginCookie(UserLogin, http.callbackData); + } else { + var error; + log("header: " + http.header); + switch(http.status) { + case 403: + if (http.getResponseHeader("content-type") == "application/json") { + var jsonResponse = http.responseText.evalJSON(false); + var perr = jsonResponse["LDAPPasswordPolicyError"]; + + // Normal password change failed + if (perr == 65535) { + error = _("Password change failed"); + } else if (perr == 3) { + error = _("Password change failed - Permission denied"); + } else if (perr == 5) { + error = _("Password change failed - Insufficient password quality"); + } else if (perr == 6) { + error = _("Password change failed - Password is too short"); + } else if (perr == 7) { + error = _("Password change failed - Password is too young"); + } else if (perr == 8) { + error = _("Password change failed - Password is in history"); + } + } else { + error = _("Unhandled error code: ") + http.status; + } + break; + case 404: + error = _("Password changing is not supported."); + break; + default: + error = _("Unhandled error code: ") + http.status; + } + showPasswordMessage(error); + } + } +} + document.observe("dom:loaded", initPreferences); diff --git a/UI/WebServerResources/generic.js b/UI/WebServerResources/generic.js index d11494780..168723060 100644 --- a/UI/WebServerResources/generic.js +++ b/UI/WebServerResources/generic.js @@ -1695,6 +1695,34 @@ AIM = { i.onComplete(d.body.innerHTML); } +}; + +function readCookie(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); + } + return null; +} + +function readLoginCookie() { + var loginValues = null; + var cookie = readCookie("0xHIGHFLYxSOGo"); + if (cookie && cookie.length > 8) { + var value = decodeURIComponent(cookie.substr(8)); + loginValues = value.base64decode().split(":"); + } + + return loginValues; +} + +function setLoginCookie(username, password) { + var value = (username + ":" + password).base64encode(); + var cookieValue = encodeURIComponent("basic " + value); + window.alert("0xHIGHFLYxSOGo=" + cookieValue); } document.observe("dom:loaded", onLoadHandler);