diff --git a/Makefile b/Makefile index 40c54fe16..1b065e417 100644 --- a/Makefile +++ b/Makefile @@ -412,6 +412,7 @@ prepare_test_server: #@cp -f e2e-tests/index.* e2e-tests/conf/ @cp -f $(SRCMANAGERDIR)/site/htdocs/manager* e2e-tests/conf/manager @cp -f $(SRCPORTALDIR)/site/htdocs/index* e2e-tests/conf/portal + @cp e2e-tests/persistent/5efe8af397fc3577e05b483aca964f1b e2e-tests/conf/persistents @cp e2e-tests/saml-sp.xml e2e-tests/conf/site/saml-sp.xml @cp e2e-tests/rules.json e2e-tests/conf/site/test.json @for f in $$(find e2e-tests/conf -name '*.fcgi'); do \ diff --git a/debian/changelog b/debian/changelog index 8d7c4750f..e9a80ca63 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,6 @@ +lemonldap-ng (2.1.0) artful; urgency=medium + + lemonldap-ng (2.0.1-1) unstable; urgency=medium * New release. See changes on our website: diff --git a/debian/control b/debian/control index 4d1e17359..bfa3ed3f5 100644 --- a/debian/control +++ b/debian/control @@ -47,7 +47,7 @@ Build-Depends-Indep: libapache-session-perl, libxml-libxslt-perl, libxml-simple-perl, perl -Standards-Version: 4.2.1 +Standards-Version: 4.3.0 Vcs-Browser: https://salsa.debian.org/perl-team/modules/packages/lemonldap-ng Vcs-Git: https://salsa.debian.org/perl-team/modules/packages/lemonldap-ng.git Homepage: https://lemonldap-ng.org/ diff --git a/e2e-tests/lemonldap-ng.ini b/e2e-tests/lemonldap-ng.ini index 0e36f9717..bc3919a9f 100644 --- a/e2e-tests/lemonldap-ng.ini +++ b/e2e-tests/lemonldap-ng.ini @@ -26,6 +26,8 @@ templateDir = __pwd__/lemonldap-ng-portal/site/templates portalStatus = 1 totp2fActivation = 1 totp2fSelfRegistration = 1 +captcha_mail_enabled = 0 +portalDisplayResetPassword = 1 [handler] diff --git a/e2e-tests/lmConf-1.json b/e2e-tests/lmConf-1.json index 695aec373..644a6bda5 100644 --- a/e2e-tests/lmConf-1.json +++ b/e2e-tests/lmConf-1.json @@ -164,7 +164,7 @@ "UA" : "$ENV{HTTP_USER_AGENT}", "_whatToTrace": "$_auth eq 'SAML' ? \"$_user\\@$_idpConfKey\" : $_auth eq 'OpenIDConnect' ? \"$_user\\@$_oidcConnectedRP\" : \"$_user\"" }, - "mailUrl": "http://auth.example.com:__port__/resetpwd", + "mailPwdRstUrl": "http://auth.example.com:__port__/resetpwd", "notification": 1, "notificationStorage": "File", "notificationStorageOptions": { diff --git a/e2e-tests/persistent/5efe8af397fc3577e05b483aca964f1b b/e2e-tests/persistent/5efe8af397fc3577e05b483aca964f1b new file mode 100644 index 000000000..b5fa19d57 --- /dev/null +++ b/e2e-tests/persistent/5efe8af397fc3577e05b483aca964f1b @@ -0,0 +1 @@ +{"_session_kind":"Persistent","_loginHistory":{"successLogin":[{"ipAddr":"127.0.0.1","_utime":1548016089}]},"_2fDevices":"[{\"type\":\"U2F\",\"_keyHandle\":\"CTPeZD3aFrNOY4yVWH4o1MKSn2aLH2OwLOWTtrQSlt_6LtUyki5nzrwBEeuxj7PRSujFZQDaMTfrEb-gr22Qfg\",\"_userKey\":\"BI1MGzKj1C9mMV8PwrYMggQXlItLBNSB19rNnFgUpLMBjAkMW8w3Sqg8s_hUGbdfdWX99duquzIzRLUtRUEvJLo\",\"name\":\"MyU2FKey\",\"epoch\":1548016193},{\"epoch\":1548016213,\"name\":\"MyYubikey\",\"_yubikey\":\"cccccchehfff\",\"type\":\"UBK\"},{\"epoch\":1548018950,\"name\":\"MyU2FKeyBlue\",\"_userKey\":\"BDEa8pQfV9agdvsX63bcwceRTXR_QvDdm5hQ5ZKQUaH4HlOi8ab4fQfl9CIACALWYm0jQcpfaRAcACiSCdwGrnI\",\"_keyHandle\":\"ZD_G6EfDv4FzttWS9RCS80SaSlRTXgtJU9r-1gInsQ4Jj1555r7nnrYhIvRfE4CTyH7NyGrt9fMnMMgByAx97Q\",\"type\":\"U2F\"}]","_session_id":"5efe8af397fc3577e05b483aca964f1b","_session_uid":"dwho","_updateTime":"20190120221550","_utime":1548016089} \ No newline at end of file diff --git a/e2e-tests/portal/00-auth.js b/e2e-tests/portal/00-auth.js index cfb4354e3..2881521d7 100644 --- a/e2e-tests/portal/00-auth.js +++ b/e2e-tests/portal/00-auth.js @@ -6,17 +6,22 @@ describe('00 Lemonldap::NG', function() { describe('Auth mechanism', function() { it('Portal should display 11 lang flags', function() { browser.driver.get('http://auth.example.com:' + process.env.TESTWEBSERVERPORT + '/'); + browser.sleep(500); browser.driver.findElements(by.className('langicon')).then(function(elems) { expect(elems.length).toEqual(11); }); + browser.sleep(500); browser.driver.findElement(by.xpath("//img[@title='en']")).click(); expect(browser.driver.findElement(by.css('[trmsg="9"]')).getText()).toEqual('Authentication required'); expect(browser.driver.findElement(by.css('[trspan="createAccount"]')).getText()).toEqual('Create an account'); + expect(browser.driver.findElement(by.css('[trspan="resetPwd"]')).getText()).toEqual('Reset my password'); browser.driver.findElement(by.xpath("//img[@title='it']")).click(); expect(browser.driver.findElement(by.css('[trmsg="9"]')).getText()).toEqual('Autenticazione necessaria'); expect(browser.driver.findElement(by.css('[trspan="createAccount"]')).getText()).toEqual('Crea un account'); + expect(browser.driver.findElement(by.css('[trspan="resetPwd"]')).getText()).toEqual('Reimpostare la password'); browser.driver.findElement(by.xpath("//img[@title='fr']")).click(); expect(browser.driver.findElement(by.css('[trspan="createAccount"]')).getText()).toEqual('Créer un compte'); + expect(browser.driver.findElement(by.css('[trspan="resetPwd"]')).getText()).toEqual('Réinitialiser mon mot de passe'); }); it('should create an account', function() { browser.driver.findElement(by.css('[trspan="createAccount"]')).click(); @@ -29,6 +34,28 @@ describe('00 Lemonldap::NG', function() { browser.driver.findElements(by.className('img-thumbnail')).then(function(elems) { expect(elems.length).toEqual(1); }); + browser.driver.findElement(by.xpath("//input[@name='firstname']")).sendKeys('doctor'); + browser.driver.findElement(by.xpath("//input[@name='lastname']")).sendKeys('who'); + browser.driver.findElement(by.xpath("//input[@name='mail']")).sendKeys('dwho@badwolf.com'); + browser.driver.findElement(by.xpath("//input[@name='captcha']")).sendKeys('1234567'); + browser.driver.findElement(by.xpath("//button[@type='submit']")).click(); + expect(browser.driver.findElement(by.css('[trmsg="76"]')).getText()).toEqual('Erreur dans la saisie du captcha'); + browser.driver.findElement(by.css('[trspan="back2Portal"]')).click(); + }); + it('should reset my password', function() { + browser.driver.findElement(by.css('[trspan="resetPwd"]')).click(); + expect(browser.driver.findElement(by.css('[trmsg="69"]')).getText()).toEqual('Merci de saisir votre adresse mail'); + expect(browser.driver.findElement(by.css('[trspan="sendPwd"]')).getText()).toEqual('Envoyez-moi un lien'); + + // A one input form + browser.driver.findElements(by.className('input-group')).then(function(elems) { + expect(elems.length).toEqual(1); + }); + browser.driver.findElement(by.xpath("//input[@name='mail']")).sendKeys('dwho@badwolf.com'); + browser.driver.findElement(by.xpath("//button[@type='submit']")).click(); + expect(browser.driver.findElement(by.css('[trmsg="72"]')).getText()).toEqual('Un mail de confirmation vous a été envoyé'); + expect(browser.driver.findElement(by.css('[trspan="mailSent2"]')).getText()).toEqual('Un message a été envoyé à votre adresse mail.'); + expect(browser.driver.findElement(by.css('[trspan="linkValidUntil"]')).getText()).toEqual("Ce message contient un lien pour réinitialiser votre mot de passe, ce lien est valide jusqu'au"); }); it('should authenticate with history', function() { expect(browser.driver.findElement(by.css('[trspan="back2Portal"]')).getText()).toEqual('Retourner au portail'); @@ -53,11 +80,11 @@ describe('00 Lemonldap::NG', function() { expect(browser.driver.findElement(by.css('[trspan="info"]')).getText()).toEqual("Information"); }); it('should display history', function() { - // Two entries + // Three entries browser.driver.findElements(by.xpath('//table/tbody/tr')).then(function(elems) { - expect(elems.length).toEqual(2); + expect(elems.length).toEqual(3); }); - // Expect history with one login and one failed login + // Expect history with two logins and one failed login browser.driver.findElements(by.xpath('//form/div/div/h3')).then(function(elems) { expect(elems.length).toEqual(3); expect(elems[0].getText()).toEqual('Information'); @@ -71,10 +98,11 @@ describe('00 Lemonldap::NG', function() { expect(elems[4].getText()).toEqual('Fehlermeldung'); }); browser.driver.findElements(by.xpath('//table/tbody/tr/td')).then(function(elems) { - expect(elems.length).toEqual(5); + expect(elems.length).toEqual(7); expect(elems[1].getText()).toEqual('127.0.0.1'); expect(elems[3].getText()).toEqual('127.0.0.1'); - expect(elems[4].getText()).toEqual('Benutzername oder Passwort nicht korrekt'); + expect(elems[5].getText()).toEqual('127.0.0.1'); + expect(elems[6].getText()).toEqual('Benutzername oder Passwort nicht korrekt'); }); expect(browser.driver.findElement(by.css('[trspan="PE5"]')).getText()).toEqual('Benutzername oder Passwort nicht korrekt'); expect(browser.driver.findElement(by.id('timer')).getText()).toMatch(/^Du wirst in \d{2} Sekunden umgeleitet$/); diff --git a/e2e-tests/portal/01-menu.js b/e2e-tests/portal/01-menu.js index 4d5d9b57c..cf2331148 100644 --- a/e2e-tests/portal/01-menu.js +++ b/e2e-tests/portal/01-menu.js @@ -1,7 +1,7 @@ 'use strict'; /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ -describe('01 Lemonldap::NG', function() { +describe('0 Lemonldap::NG', function() { describe('Portal should display Menu', function() { it('Should have four buttons', function() { browser.driver.findElement(by.xpath("//img[@title='fr']")).click(); @@ -42,10 +42,11 @@ describe('01 Lemonldap::NG', function() { expect(elems[4].getText()).toEqual("Message d'erreur"); }); browser.driver.findElements(by.xpath('//table/tbody/tr/td')).then(function(elems) { - expect(elems.length).toEqual(5); + expect(elems.length).toEqual(7); expect(elems[1].getText()).toEqual('127.0.0.1'); expect(elems[3].getText()).toEqual('127.0.0.1'); - expect(elems[4].getText()).toEqual('Mot de passe ou identifiant incorrect'); + expect(elems[5].getText()).toEqual('127.0.0.1'); + expect(elems[6].getText()).toEqual('Mot de passe ou identifiant incorrect'); }); browser.driver.findElement(by.xpath("//button[@type='button']")).click(); browser.sleep(1000); diff --git a/e2e-tests/portal/10-sfaManager.js b/e2e-tests/portal/10-sfaManager.js index c18192a9c..8088acce9 100644 --- a/e2e-tests/portal/10-sfaManager.js +++ b/e2e-tests/portal/10-sfaManager.js @@ -27,7 +27,36 @@ describe('10 Lemonldap::NG', function() { browser.sleep(1000); }); }); - it('Should submit TOTP form', function() { + it('Should display 2FA Manager', function() { + expect(browser.driver.findElement(by.css('[trspan="choose2f"]')).getText()).toEqual('Choisissez votre second facteur'); + browser.driver.findElements(by.xpath('//table/thead/tr/th')).then(function(elems) { + expect(elems.length).toEqual(4); + expect(elems[0].getText()).toEqual('Type'); + expect(elems[1].getText()).toEqual('Nom'); + expect(elems[2].getText()).toEqual('Date'); + // expect(elems[3].getText()).toEqual('Action'); + }); + browser.driver.findElements(by.xpath('//table/tbody/tr/td')).then(function(elems) { + expect(elems.length).toEqual(12); + expect(elems[0].getText()).toEqual('U2F'); + expect(elems[1].getText()).toEqual('MyU2FKey'); + expect(elems[2].getText()).toEqual('20/01/2019 à 21:29:53'); + expect(elems[4].getText()).toEqual('UBK'); + expect(elems[5].getText()).toEqual('MyYubikey'); + expect(elems[6].getText()).toEqual('20/01/2019 à 21:30:13'); + expect(elems[8].getText()).toEqual('U2F'); + expect(elems[9].getText()).toEqual('MyU2FKeyBlue'); + expect(elems[10].getText()).toEqual('20/01/2019 à 22:15:50'); + }); + expect(browser.driver.findElement(by.className('card-footer')).getText()).toEqual('TOTP2F'); + browser.driver.findElements(by.className('btn-danger')).then(function(elems) { + expect(elems.length).toEqual(0); + // elems[0].click(); + }); + // expect(browser.driver.findElement(by.css('[trspan="choose2f"]')).getText()).toEqual("Vous n'êtes pas autorisé à faire cette requête"); + browser.driver.findElement(by.xpath("//img[@title='totp2F']")).click(); + }); + it('Should display and submit TOTP form', function() { browser.driver.findElements(by.css('[role="button"]')).then(function(links) { expect(links.length).toEqual(4); expect(links[0].getText()).toEqual('Générer une nouvelle clef'); @@ -56,7 +85,7 @@ describe('10 Lemonldap::NG', function() { // Back to Portal links[3].click(); browser.driver.findElement(by.xpath("//button[@type='button']")).click(); - browser.sleep(1000); + browser.sleep(500); expect(browser.driver.findElement(by.css('[trspan="yourApps"]')).getText()).toEqual('Vos applications'); }); }); diff --git a/e2e-tests/portal/99-logout.js b/e2e-tests/portal/99-logout.js index 5b8841685..842cee848 100644 --- a/e2e-tests/portal/99-logout.js +++ b/e2e-tests/portal/99-logout.js @@ -3,9 +3,17 @@ /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ describe('99 Lemonldap::NG auth mechanism', function() { - it('should allow logout', function() { - browser.driver.get('http://auth.example.com:' + process.env.TESTWEBSERVERPORT + '/?logout=1'); + browser.driver.findElements(by.xpath('//li/a/span/img')).then(function(links) { + expect(links.length).toEqual(4); + links[3].click(); + browser.sleep(1000); + expect(browser.driver.findElement(by.css('[trspan="areYouSure"]')).getText()).toEqual('Êtes-vous sûr ?'); + browser.driver.findElement(by.css('[trspan="imSure"]')).click(); + expect(browser.driver.findElement(by.css('[trmsg="47"]')).getText()).toEqual('Vous avez été déconnecté'); + browser.sleep(500); + browser.driver.findElement(by.css('[trspan="goToPortal"]')).click(); + expect(browser.driver.findElement(by.css('[trmsg="9"]')).getText()).toEqual('Veuillez vous authentifier'); + }); }); - }); \ No newline at end of file diff --git a/e2e-tests/protractor-conf.js b/e2e-tests/protractor-conf.js index 35fec1a71..c18b16e97 100644 --- a/e2e-tests/protractor-conf.js +++ b/e2e-tests/protractor-conf.js @@ -2,11 +2,12 @@ exports.config = { allScriptsTimeout: 300000, // Specific test - // specs: process.env.E2E_TESTS, + specs: process.env.E2E_TESTS, // All tests - specs: ['portal/*.js', 'handler/*.js', 'manager/*.js' ], + //specs: ['portal/*.js', 'handler/*.js', 'manager/*.js' ], capabilities: { + //'browserName': 'firefox' 'browserName': 'chrome' }, diff --git a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm index 53d58f12c..1d810a8d0 100644 --- a/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm +++ b/lemonldap-ng-common/lib/Lemonldap/NG/Common/Conf/DefaultValues.pm @@ -121,9 +121,9 @@ sub defaultValues { 'macros' => {}, 'mailCharset' => 'utf-8', 'mailFrom' => 'noreply@example.com', + 'mailPwdRstTimeout' => 0, 'mailPwdRstUrl' => 'http://auth.example.com/resetpwd', 'mailSessionKey' => 'mail', - 'mailTimeout' => 0, 'managerDn' => '', 'managerPassword' => '', 'max2FDevices' => 10, diff --git a/lemonldap-ng-common/t/02-Common-Conf-File.t b/lemonldap-ng-common/t/02-Common-Conf-File.t index afa281ec7..ebd5f5b38 100644 --- a/lemonldap-ng-common/t/02-Common-Conf-File.t +++ b/lemonldap-ng-common/t/02-Common-Conf-File.t @@ -16,10 +16,8 @@ BEGIN { use_ok('Lemonldap::NG::Common::Conf') } my $h; -ok( - $h = new Lemonldap::NG::Common::Conf( - { - type => 'File', +ok( $h = new Lemonldap::NG::Common::Conf( + { type => 'File', dirName => "t/", } ), @@ -39,25 +37,15 @@ my @test = ( { cfgNum => 1, test => 'éà' } ); -for ( my $i = 0 ; $i < @test ; $i++ ) { +for ( my $i = 0; $i < @test; $i++ ) { ok( $h->store( $test[$i] ) == 1, "Test $i is stored" ) - or print STDERR "$Lemonldap::NG::Common::Conf::msg $!"; + or print STDERR "$Lemonldap::NG::Common::Conf::msg $!"; $count++; - if ( -x '/usr/bin/file' ) { - eval { - open F, 'file t/lmConf-1.json |'; - $_ = join( '', ); - close F; - ok( /(ascii|utf-?8)/si, "File is $1 encoded" ) - or print STDERR "Result: $_\n"; - $count++; - }; - } my $cfg; ok( $cfg = $h->load(1), "Test $i can be read" ) - or print STDERR $Lemonldap::NG::Common::Conf::msg; + or print STDERR $Lemonldap::NG::Common::Conf::msg; ok( $cfg->{test} eq $test[$i]->{test}, "Test $i is restored" ) - or print STDERR "Expect $cfg->{test} eq $test[$i]->{test}\n"; + or print STDERR "Expect $cfg->{test} eq $test[$i]->{test}\n"; $count += 2; } diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm index a5e0541da..debacae74 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/2ndFA.pm @@ -243,14 +243,6 @@ sub sfa { $self->logger->debug( "Removing sessions unless a $_ device is registered"); } - - #else { - # ( - # return $self->sendError( - # $req, "Bad or Missing " . $_ . "Check parameter", 400 - # ) - # ); - #} } my $total = ( keys %$res ); diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm index 7823c22e3..5af0e74cb 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Attributes.pm @@ -1585,6 +1585,10 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][ 'mailPwdRstSubject' => { 'type' => 'text' }, + 'mailPwdRstTimeout' => { + 'default' => 0, + 'type' => 'int' + }, 'mailPwdRstUrl' => { 'default' => 'http://auth.example.com/resetpwd', 'type' => 'url' @@ -1596,10 +1600,6 @@ qr/^(?:\*\.)?(?:(?:(?:(?:[a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*(?:[a-zA-Z][ 'default' => 'mail', 'type' => 'text' }, - 'mailTimeout' => { - 'default' => 0, - 'type' => 'int' - }, 'maintenance' => { 'default' => 0, 'type' => 'bool' diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm index 6a60dd965..f66f3f21a 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Attributes.pm @@ -1137,19 +1137,19 @@ sub attributes { }, mailReplyTo => { type => 'text', documentation => 'Reply-To address' }, - mailTimeout => { + mailPwdRstTimeout => { type => 'int', default => 0, - documentation => 'Mail session timeout', + documentation => 'Mail password reset session timeout', }, # Password reset mailPwdRstBody => - { type => 'longtext', documentation => 'Custom mail body', }, + { type => 'longtext', documentation => 'Custom password reset mail body', }, mailPwdRstConfirmBody => { type => 'longtext', - documentation => 'Custom confirm mail body', + documentation => 'Custom confirm password reset mail body', }, mailPwdRstConfirmSubject => { type => 'text', diff --git a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm index 150f1d3b7..acd16cf30 100644 --- a/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm +++ b/lemonldap-ng-manager/lib/Lemonldap/NG/Manager/Build/Tree.pm @@ -113,6 +113,7 @@ sub tree { nodes_cond => [ { title => 'adParams', help => 'authad.html', + form => 'simpleInputContainer', nodes => [ 'ADPwdMaxAge', 'ADPwdExpireWarning' ] }, { title => 'choiceParams', @@ -201,6 +202,7 @@ sub tree { ] }, { title => 'kerberosParams', + form => 'simpleInputContainer', help => 'authkerberos.html', nodes => [ 'krbKeytab', 'krbByJs', @@ -506,11 +508,14 @@ sub tree { 'upgradeSession', { title => 'portalServers', help => 'portalservers.html', - form => 'simpleInputContainer', nodes => [ - 'wsdlServer', 'restSessionServer', - 'restConfigServer', 'soapSessionServer', - 'soapConfigServer', 'exportedAttr' + 'wsdlServer', + 'restSessionServer', + 'restConfigServer', + 'soapSessionServer', + 'soapConfigServer', + 'exportedAttr', + ] }, { title => 'loginHistory', @@ -537,20 +542,6 @@ sub tree { { title => 'passwordManagement', help => 'resetpassword.html', nodes => [ - { title => 'SMTP', - nodes => [ - 'SMTPServer', 'SMTPPort', - 'SMTPAuthUser', 'SMTPAuthPass', - 'SMTPTLS', 'SMTPTLSOpts', - ] - }, - { title => 'mailHeaders', - form => 'simpleInputContainer', - nodes => [ - 'mailFrom', 'mailReplyTo', - 'mailCharset' - ] - }, { title => 'mailPwdRstContent', form => 'simpleInputContainer', nodes => [ @@ -564,9 +555,8 @@ sub tree { form => 'simpleInputContainer', nodes => [ 'mailPwdRstUrl', + 'mailPwdRstTimeout', 'randomPasswordRegexp', - 'mailTimeout', - 'mailSessionKey' ] } ] @@ -621,25 +611,32 @@ sub tree { help => 'u2f.html', form => 'simpleInputContainer', nodes => [ - 'u2fActivation', 'u2fSelfRegistration', - 'u2fAuthnLevel', 'u2fUserCanRemoveKey', + 'u2fActivation', + 'u2fSelfRegistration', + 'u2fAuthnLevel', + 'u2fUserCanRemoveKey', ] }, { title => 'external2f', help => 'external2f.html', form => 'simpleInputContainer', nodes => [ - 'ext2fActivation', 'ext2FSendCommand', - 'ext2FValidateCommand', 'ext2fAuthnLevel', + 'ext2fActivation', + 'ext2FSendCommand', + 'ext2FValidateCommand', + 'ext2fAuthnLevel', 'ext2fLogo', ] }, { title => 'rest2f', help => 'rest2f.html', nodes => [ - 'rest2fActivation', 'rest2fInitUrl', - 'rest2fInitArgs', 'rest2fVerifyUrl', - 'rest2fVerifyArgs', 'rest2fAuthnLevel', + 'rest2fActivation', + 'rest2fInitUrl', + 'rest2fInitArgs', + 'rest2fVerifyUrl', + 'rest2fVerifyArgs', + 'rest2fAuthnLevel', 'rest2fLogo', ] }, @@ -666,6 +663,26 @@ sub tree { nodes => [ 'customFunctions', 'multiValuesSeparator', + { title => 'SMTP', + help => 'smtp.html', + nodes => [ + 'mailSessionKey', + 'SMTPServer', + 'SMTPPort', + 'SMTPAuthUser', + 'SMTPAuthPass', + 'SMTPTLS', + 'SMTPTLSOpts', + { title => 'mailHeaders', + form => 'simpleInputContainer', + nodes => [ + 'mailFrom', + 'mailReplyTo', + 'mailCharset' + ] + }, + ] + }, { title => 'security', help => 'security.html#configure_security_settings', @@ -710,17 +727,20 @@ sub tree { help => 'redirections.html#portal_redirections', form => 'simpleInputContainer', nodes => [ - 'jsRedirect', 'noAjaxHook', + 'jsRedirect', + 'noAjaxHook', 'skipRenewConfirmation', ] }, 'nginxCustomHandlers', - 'logoutServices', + 'logoutServices', { title => 'forms', form => 'simpleInputContainer', nodes => [ - 'infoFormMethod', 'confirmFormMethod', - 'redirectFormMethod', 'activeTimer', + 'infoFormMethod', + 'confirmFormMethod', + 'redirectFormMethod', + 'activeTimer', ] }, ] @@ -932,7 +952,9 @@ sub tree { { title => 'casServiceMetadata', nodes => [ 'casAttr', - 'casAccessControlPolicy', 'casStorage', 'casStorageOptions', + 'casAccessControlPolicy', + 'casStorage', + 'casStorageOptions', 'casAttributes', ] diff --git a/lemonldap-ng-manager/site/coffee/filterFunctions.coffee b/lemonldap-ng-manager/site/coffee/filterFunctions.coffee index 0f07fa349..c7629c30e 100644 --- a/lemonldap-ng-manager/site/coffee/filterFunctions.coffee +++ b/lemonldap-ng-manager/site/coffee/filterFunctions.coffee @@ -20,11 +20,11 @@ filterFunctions = # Little function to select good node p = (s) -> tmp = s.toLowerCase() - if tmp == 'ad' - tmp = 'ldap' - else if tmp == 'openidconnect' + if tmp == 'openidconnect' tmp = 'oidc' nToShow.push tmp + 'Params' + if tmp == 'ad' + nToShow.push 'ldapParams' # Show all normal nodes for n in node.nodes diff --git a/lemonldap-ng-manager/site/htdocs/static/forms/menuCat.html b/lemonldap-ng-manager/site/htdocs/static/forms/menuCat.html index f483c1266..f1b06adc6 100644 --- a/lemonldap-ng-manager/site/htdocs/static/forms/menuCat.html +++ b/lemonldap-ng-manager/site/htdocs/static/forms/menuCat.html @@ -20,7 +20,22 @@ - + + \ No newline at end of file diff --git a/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.js b/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.js index a47e8cba7..08eb78a72 100644 --- a/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.js +++ b/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.js @@ -1,4 +1,4 @@ -// Generated by CoffeeScript 1.12.8 +// Generated by CoffeeScript 1.12.7 (function() { var filterFunctions; @@ -18,12 +18,13 @@ p = function(s) { var tmp; tmp = s.toLowerCase(); - if (tmp === 'ad') { - tmp = 'ldap'; - } else if (tmp === 'openidconnect') { + if (tmp === 'openidconnect') { tmp = 'oidc'; } - return nToShow.push(tmp + 'Params'); + nToShow.push(tmp + 'Params'); + if (tmp === 'ad') { + return nToShow.push('ldapParams'); + } }; ref1 = node.nodes; for (j = 0, len1 = ref1.length; j < len1; j++) { diff --git a/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.min.js b/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.min.js index 4e92532d8..d8ededb83 100644 --- a/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.min.js +++ b/lemonldap-ng-manager/site/htdocs/static/js/filterFunctions.min.js @@ -1 +1 @@ -(function(){var filterFunctions;filterFunctions={authParams:function(scope,$q,node){var i,len,n,ref,wait;wait=[];ref=node.nodes;for(i=0,len=ref.length;i ( is => 'rw', lazy => 1, default => sub { - my $ott = - $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); + my $ott = $_[0]->{p} + ->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken'); $ott->timeout( $_[0]->{conf}->{formTimeout} ); return $ott; } @@ -55,7 +55,7 @@ sub init { ? 'available2FSelfRegistration' : 'available2F' } - ) + ) { my $prefix = lc($_); $prefix =~ s/2f$//i; @@ -67,9 +67,10 @@ sub init { # Unless $rule, skip loading if ( $self->conf->{$ap} ) { $self->logger->debug("Trying to load $_ 2F"); - my $m = - $self->p->loadPlugin( $i ? "::2F::Register::$_" : "::2F::$_" ) - or return 0; + my $m + = $self->p->loadPlugin( + $i ? "::2F::Register::$_" : "::2F::$_" ) + or return 0; # Rule and prefix may be modified by 2F module, reread them my $rule = $self->conf->{$ap}; @@ -79,13 +80,13 @@ sub init { $rule = $self->p->HANDLER->substitute($rule); unless ( $rule = $self->p->HANDLER->buildSub($rule) ) { $self->error( 'External 2F rule error: ' - . $self->p->HANDLER->tsv->{jail}->error ); + . $self->p->HANDLER->tsv->{jail}->error ); return 0; } # Store module push @{ $self->{ $i ? 'sfRModules' : 'sfModules' } }, - { p => $prefix, m => $m, r => $rule }; + { p => $prefix, m => $m, r => $rule }; } else { $self->logger->debug(' -> not enabled'); @@ -99,10 +100,10 @@ sub init { $self->p->HANDLER->substitute( $self->conf->{sfRequired} ) ) ) - ) + ) { $self->error( 'Error in sfRequired rule' - . $self->p->HANDLER->tsv->{jail}->error ); + . $self->p->HANDLER->tsv->{jail}->error ); return 0; } @@ -163,13 +164,12 @@ sub run { if ( $self->sfReq->( $req, $req->sessionInfo ) ) { $self->logger->debug("2F is required..."); $self->logger->debug(" -> Register 2F"); - $req->pdata->{sfRegToken} = - $self->ott->createToken( $req->sessionInfo ); + $req->pdata->{sfRegToken} + = $self->ott->createToken( $req->sessionInfo ); $self->logger->debug("Just one 2F is enabled"); $self->logger->debug(" -> Redirect to 2fregisters/"); $req->response( - [ - 302, + [ 302, [ Location => $self->conf->{portal} . '2fregisters/' ], [] ] ); @@ -181,7 +181,7 @@ sub run { } $self->userLogger->info( 'Second factor required for ' - . $req->sessionInfo->{ $self->conf->{whatToTrace} } ); + . $req->sessionInfo->{ $self->conf->{whatToTrace} } ); # Store user data in a token $req->sessionInfo->{_2fRealSession} = $req->id; @@ -206,7 +206,8 @@ sub run { MAIN_LOGO => $self->conf->{portalMainLogo}, SKIN => $self->conf->{portalSkin}, TOKEN => $token, - MODULES => [ map { { CODE => $_->prefix, LOGO => $_->logo } } @am ], + MODULES => + [ map { { CODE => $_->prefix, LOGO => $_->logo } } @am ], CHECKLOGINS => $checkLogins } ); @@ -232,15 +233,16 @@ sub _choice { # Restore session unless ( $token = $req->param('token') ) { - $self->userLogger->error( $self->prefix . ' 2F access without token' ); + $self->userLogger->error( + $self->prefix . ' 2F access without token' ); $req->mustRedirect(1); - return $self->p->do( $req, [ sub { PE_NOTOKEN } ] ); + return $self->p->do( $req, [ sub {PE_NOTOKEN} ] ); } my $session; unless ( $session = $self->ott->getToken($token) ) { $self->userLogger->info('Token expired'); - return $self->p->do( $req, [ sub { PE_TOKENEXPIRED } ] ); + return $self->p->do( $req, [ sub {PE_TOKENEXPIRED} ] ); } $req->sessionInfo($session); @@ -255,8 +257,7 @@ sub _choice { $req->authResult($res); return $self->p->do( $req, - [ - sub { $res }, 'controlUrl', + [ sub {$res}, 'controlUrl', 'buildCookie', @{ $self->p->endAuth }, ] ); @@ -271,10 +272,7 @@ sub _redirect { my $arg = $req->env->{QUERY_STRING}; $self->logger->debug('Call sfEngine _redirect method'); return [ - 302, - [ - Location => $self->conf->{portal} . ( $arg ? "?$arg" : '' ) - ], + 302, [ Location => $self->conf->{portal} . ( $arg ? "?$arg" : '' ) ], [] ]; } @@ -286,8 +284,8 @@ sub _displayRegister { # - display template if $tpl # - else display choice template if ($tpl) { - my ($m) = - grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules }; + my ($m) + = grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules }; unless ($m) { return $self->p->sendError( $req, 'Inexistent register module', 400 ); @@ -307,34 +305,56 @@ sub _displayRegister { 'Looking if ' . $m->{m}->prefix . '2F register is available' ); if ( $m->{r}->( $req, $req->userData ) ) { push @am, - { + { CODE => $m->{m}->prefix, URL => '/2fregisters/' . $m->{m}->prefix, LOGO => $m->{m}->logo, - }; + }; } } - if ( - @am == 1 + if (@am == 1 and not( $req->userData->{_2fDevices} or $req->data->{sfRegRequired} ) - ) + ) { return [ 302, [ Location => $self->conf->{portal} . $am[0]->{URL} ], [] ]; } - my $_2fDevices = - $req->userData->{_2fDevices} - ? eval { from_json( $req->userData->{_2fDevices}, - { allow_nonref => 1 } ); } - : undef; - + # Retrieve user all second factors + my $_2fDevices = $req->userData->{_2fDevices} + ? eval { + from_json( $req->userData->{_2fDevices}, { allow_nonref => 1 } ); + } + : undef; unless ($_2fDevices) { $self->logger->debug("No 2F Device found"); $_2fDevices = []; } + # Parse second factors to display delete button if allowed + my $action = ''; + foreach + my $type ( split /,\s*/, $self->conf->{available2FSelfRegistration} ) + + { + foreach (@$_2fDevices) { + $_->{type} =~ s/^UBK$/Yubikey/; + if ( $_->{type} eq $type ) { + my $t = lc($type); + $t =~ s/2f$//i; + + $_->{delAllowed} + = $self->conf->{ $t . '2fActivation' } + && $self->conf->{ $t . '2fUserCanRemoveKey' } + && $self->conf->{ $t . '2fSelfRegistration' }; + } + $action ||= $_->{delAllowed}; + $_->{type} =~ s/^Yubikey$/UBK/; + } + } + + # Display template return $self->p->sendHtml( $req, '2fregisters', @@ -343,6 +363,7 @@ sub _displayRegister { SKIN => $self->conf->{portalSkin}, MODULES => \@am, SFDEVICES => $_2fDevices, + ACTION => $action, REG_REQUIRED => $req->data->{sfRegRequired}, } ); @@ -356,8 +377,8 @@ sub register { # - call register run method if $tpl # - else give JSON list of available registers for this user if ($tpl) { - my ($m) = - grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules }; + my ($m) + = grep { $_->{m}->prefix eq $tpl } @{ $self->sfRModules }; unless ($m) { return $self->p->sendError( $req, 'Inexistent register module', 400 ); @@ -376,11 +397,11 @@ sub register { $self->logger->debug(' -> OK'); my $name = $m->{m}->prefix; push @am, - { + { name => $name, logo => $m->{m}->logo, url => "/2fregisters/$name" - }; + }; } } return $self->p->sendJSONresponse( $req, \@am ); @@ -389,12 +410,12 @@ sub register { sub restoreSession { my ( $self, $req, @path ) = @_; my $token = $req->pdata->{sfRegToken} - or return [ 302, [ Location => $self->conf->{portal} ], [] ]; + or return [ 302, [ Location => $self->conf->{portal} ], [] ]; $req->userData( $self->ott->getToken( $token, 1 ) ); $req->data->{sfRegRequired} = 1; return $req->method eq 'POST' - ? $self->register( $req, @path ) - : $self->_displayRegister( $req, @path ); + ? $self->register( $req, @path ) + : $self->_displayRegister( $req, @path ); } 1; diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm index 010da1a01..676aaf7af 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Main/Display.pm @@ -287,7 +287,7 @@ sub display { ASK_LOGINS => $req->param('checkLogins') || 0, DISPLAY_RESETPASSWORD => $self->conf->{portalDisplayResetPassword}, DISPLAY_REGISTER => $self->conf->{portalDisplayRegister}, - MAIL_URL => $self->conf->{mailUrl}, + MAIL_URL => $self->conf->{mailPwdRstUrl}, REGISTER_URL => $self->conf->{registerUrl}, HIDDEN_INPUTS => $self->buildHiddenForm($req), STAYCONNECTED => $self->conf->{stayConnected}, diff --git a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm index b3cbc2e3e..45973a072 100644 --- a/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm +++ b/lemonldap-ng-portal/lib/Lemonldap/NG/Portal/Plugins/MailPasswordReset.pm @@ -203,9 +203,9 @@ sub _reset { return PE_MAILNOTFOUND; } - my $mailTimeout - = $self->conf->{mailTimeout} || $self->conf->{timeout}; - my $expTimestamp = time() + $mailTimeout; + my $mailPwdRstTimeout + = $self->conf->{mailPwdRstTimeout} || $self->conf->{timeout}; + my $expTimestamp = time() + $mailPwdRstTimeout; $req->data->{expMailDate} = strftime( "%d/%m/%Y", localtime $expTimestamp ); $req->data->{expMailTime} @@ -227,12 +227,12 @@ sub _reset { # Use default session timeout and mail session timeout to compute it my $time = time(); my $timeout = $self->conf->{timeout}; - my $mailTimeout = $self->conf->{mailTimeout} || $timeout; + my $mailPwdRstTimeout = $self->conf->{mailPwdRstTimeout} || $timeout; - $infos->{_utime} = $time + ( $mailTimeout - $timeout ); + $infos->{_utime} = $time + ( $mailPwdRstTimeout - $timeout ); # Store expiration timestamp for further use - $infos->{mailSessionTimeoutTimestamp} = $time + $mailTimeout; + $infos->{mailSessionTimeoutTimestamp} = $time + $mailPwdRstTimeout; # Store start timestamp for further use $infos->{mailSessionStartTimestamp} = $time; diff --git a/lemonldap-ng-portal/site/coffee/2fregistration.coffee b/lemonldap-ng-portal/site/coffee/2fregistration.coffee index b1ef1cac4..28997fce5 100644 --- a/lemonldap-ng-portal/site/coffee/2fregistration.coffee +++ b/lemonldap-ng-portal/site/coffee/2fregistration.coffee @@ -15,7 +15,10 @@ displayError = (j, status, err) -> if res and res.error res = res.error.replace /.* /, '' console.log 'Returned error', res - setMsg res, 'warning' + if res.match /module/ + setMsg 'notAuthorized', 'warning' + else + setMsg res, 'warning' # Delete function (launched by "delete" button) delete2F = (device, epoch) -> diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js b/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js index 025402ee8..c18ec9eb4 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.js @@ -24,7 +24,11 @@ LemonLDAP::NG 2F registration script if (res && res.error) { res = res.error.replace(/.* /, ''); console.log('Returned error', res); - return setMsg(res, 'warning'); + if (res.match(/module/)) { + return setMsg('notAuthorized', 'warning'); + } else { + return setMsg(res, 'warning'); + } } }; diff --git a/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.min.js b/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.min.js index 35ed57444..b78e564ec 100644 --- a/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.min.js +++ b/lemonldap-ng-portal/site/htdocs/static/common/js/2fregistration.min.js @@ -1 +1 @@ -(function(){var delete2F,displayError,setMsg;setMsg=function(msg,level){$("#msg").html(window.translate(msg));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+level);if(level==="positive"){level="success"}return $("#color").addClass("alert-"+level)};displayError=function(j,status,err){var res;console.log("Error",err);res=JSON.parse(j.responseText);if(res&&res.error){res=res.error.replace(/.* /,"");console.log("Returned error",res);return setMsg(res,"warning")}};delete2F=function(device,epoch){if(device==="U2F"){device="u"}else if(device==="UBK"){device="yubikey"}else if(device==="TOTP"){device="totp"}else{setMsg("u2fFailed","warning")}return $.ajax({type:"POST",url:portal+"2fregisters/"+device+"/delete",data:{epoch:epoch},dataType:"json",error:displayError,success:function(resp){if(resp.error){if(resp.error.match(/notAuthorized/)){return setMsg("notAuthorized","warning")}else{return setMsg("unknownAction","warning")}}else if(resp.result){$("#delete-"+epoch).hide();return setMsg("yourKeyIsUnregistered","positive")}},error:displayError})};$(document).ready(function(){$("body").on("click",".btn-danger",function(){return delete2F($(this).attr("device"),$(this).attr("epoch"))});$("#goback").attr("href",portal);return $(".data-epoch").each(function(){var myDate;myDate=new Date($(this).text()*1e3);return $(this).text(myDate.toLocaleString())})})}).call(this); +(function(){var delete2F,displayError,setMsg;setMsg=function(msg,level){$("#msg").html(window.translate(msg));$("#color").removeClass("message-positive message-warning alert-success alert-warning");$("#color").addClass("message-"+level);if(level==="positive"){level="success"}return $("#color").addClass("alert-"+level)};displayError=function(j,status,err){var res;console.log("Error",err);res=JSON.parse(j.responseText);if(res&&res.error){res=res.error.replace(/.* /,"");console.log("Returned error",res);if(res.match(/module/)){return setMsg("notAuthorized","warning")}else{return setMsg(res,"warning")}}};delete2F=function(device,epoch){if(device==="U2F"){device="u"}else if(device==="UBK"){device="yubikey"}else if(device==="TOTP"){device="totp"}else{setMsg("u2fFailed","warning")}return $.ajax({type:"POST",url:portal+"2fregisters/"+device+"/delete",data:{epoch:epoch},dataType:"json",error:displayError,success:function(resp){if(resp.error){if(resp.error.match(/notAuthorized/)){return setMsg("notAuthorized","warning")}else{return setMsg("unknownAction","warning")}}else if(resp.result){$("#delete-"+epoch).hide();return setMsg("yourKeyIsUnregistered","positive")}},error:displayError})};$(document).ready(function(){$("body").on("click",".btn-danger",function(){return delete2F($(this).attr("device"),$(this).attr("epoch"))});$("#goback").attr("href",portal);return $(".data-epoch").each(function(){var myDate;myDate=new Date($(this).text()*1e3);return $(this).text(myDate.toLocaleString())})})}).call(this); diff --git a/lemonldap-ng-portal/site/templates/bootstrap/2fregisters.tpl b/lemonldap-ng-portal/site/templates/bootstrap/2fregisters.tpl index 96d0de49a..35e6850c3 100644 --- a/lemonldap-ng-portal/site/templates/bootstrap/2fregisters.tpl +++ b/lemonldap-ng-portal/site/templates/bootstrap/2fregisters.tpl @@ -17,7 +17,10 @@ Type Name Date - Action + + + Action + @@ -27,11 +30,13 @@ - - - Unregister - - + + + + Unregister + + + diff --git a/lemonldap-ng-portal/site/templates/bootstrap/gpgform.tpl b/lemonldap-ng-portal/site/templates/bootstrap/gpgform.tpl index 36eb7045e..75d181337 100644 --- a/lemonldap-ng-portal/site/templates/bootstrap/gpgform.tpl +++ b/lemonldap-ng-portal/site/templates/bootstrap/gpgform.tpl @@ -23,7 +23,7 @@
- + " /> diff --git a/lemonldap-ng-portal/site/templates/bootstrap/mail.tpl b/lemonldap-ng-portal/site/templates/bootstrap/mail.tpl index e2848298b..df35a5e88 100644 --- a/lemonldap-ng-portal/site/templates/bootstrap/mail.tpl +++ b/lemonldap-ng-portal/site/templates/bootstrap/mail.tpl @@ -36,7 +36,7 @@
- + @@ -87,7 +87,7 @@
- +
diff --git a/lemonldap-ng-portal/site/templates/bootstrap/register.tpl b/lemonldap-ng-portal/site/templates/bootstrap/register.tpl index aec5d8743..d9cbc2be6 100644 --- a/lemonldap-ng-portal/site/templates/bootstrap/register.tpl +++ b/lemonldap-ng-portal/site/templates/bootstrap/register.tpl @@ -48,7 +48,7 @@
- + diff --git a/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl b/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl index 216201ff0..5b0293c73 100644 --- a/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl +++ b/lemonldap-ng-portal/site/templates/bootstrap/standardform.tpl @@ -21,7 +21,7 @@
- +