diff --git a/base-no-s6/2.23.3.patch b/base-no-s6/2.23.3.patch
new file mode 100644
index 0000000..e0acbbd
--- /dev/null
+++ b/base-no-s6/2.23.3.patch
@@ -0,0 +1,106 @@
+--- a/usr/share/perl5/Lemonldap/NG/Common/EmailTransport.pm
++++ b/usr/share/perl5/Lemonldap/NG/Common/EmailTransport.pm
+@@ -6,7 +6,53 @@ use MIME::Entity;
+ use Email::Sender::Simple qw(sendmail);
+ use Email::Date::Format qw(email_date);
+
+-our $VERSION = '2.23.0';
++our $VERSION = '2.23.3';
++
++# Check that SMTPAuthMech can be honored: it requires Authen::SASL and an
++# Email::Sender version providing the sasl_authenticator attribute (>= 1.300032)
++# Returns an error message, or undef when the configuration is usable
++sub checkSasl {
++ my ( $class, $conf, $transportClass ) = @_;
++ return undef unless $conf->{SMTPAuthMech} and $conf->{SMTPAuthUser};
++ $transportClass ||= 'Email::Sender::Transport::SMTP';
++ return "Choosing the SASL mechanism (SMTPAuthMech) is not supported by "
++ . "$transportClass, Email::Sender 1.300032 or higher is required"
++ unless $transportClass->can('sasl_authenticator');
++ eval { require Authen::SASL; };
++ return "Choosing the SASL mechanism (SMTPAuthMech) requires Authen::SASL"
++ if $@;
++ return undef;
++}
++
++# Build the SASL related arguments given to the transport constructor.
++# When SMTPAuthMech is set, an Authen::SASL object restricted to the wanted
++# mechanism(s) is used instead of sasl_username/sasl_password: else Net::SMTP
++# builds itself a SASL client using all the mechanisms advertised by the
++# server, and picks the "strongest" one, which may be broken server side
++# (DIGEST-MD5 on some providers for example).
++# NB: sasl_authenticator and sasl_username are mutually exclusive.
++sub _saslArgs {
++ my ( $transportClass, $conf ) = @_;
++ return () unless $conf->{SMTPAuthUser};
++ if ( $conf->{SMTPAuthMech} ) {
++ my $error = __PACKAGE__->checkSasl( $conf, $transportClass );
++ die "$error\n" if $error;
++ return (
++ sasl_authenticator => Authen::SASL->new(
++ mechanism => $conf->{SMTPAuthMech},
++ callback => {
++ user => $conf->{SMTPAuthUser},
++ authname => $conf->{SMTPAuthUser},
++ pass => $conf->{SMTPAuthPass},
++ },
++ )
++ );
++ }
++ return (
++ sasl_username => $conf->{SMTPAuthUser},
++ sasl_password => $conf->{SMTPAuthPass},
++ );
++}
+
+ sub new {
+ my ( $class, $conf ) = @_;
+@@ -25,14 +71,7 @@ sub new {
+ $transport = Email::Sender::Transport::SMTPS->new(
+ host => $conf->{SMTPServer},
+ ( $conf->{SMTPPort} ? ( port => $conf->{SMTPPort} ) : () ),
+- (
+- $conf->{SMTPAuthUser}
+- ? (
+- sasl_username => $conf->{SMTPAuthUser},
+- sasl_password => $conf->{SMTPAuthPass}
+- )
+- : ()
+- ),
++ _saslArgs( 'Email::Sender::Transport::SMTPS', $conf ),
+ ssl => $smtpTls,
+ );
+ return $transport;
+@@ -49,17 +88,11 @@ sub new {
+ $transport = Email::Sender::Transport::SMTP->new(
+ host => $conf->{SMTPServer},
+ ( $conf->{SMTPPort} ? ( port => $conf->{SMTPPort} ) : () ),
+- (
+- $conf->{SMTPAuthUser}
+- ? (
+- sasl_username => $conf->{SMTPAuthUser},
+- sasl_password => $conf->{SMTPAuthPass}
+- )
+- : ()
+- ),
++ _saslArgs( 'Email::Sender::Transport::SMTP', $conf ),
+ ( $smtpTls ? ( ssl => $smtpTls ) : () ),
+ (
+- $conf->{SMTPTLSOpts} ? ( ssl_options => $conf->{SMTPTLSOpts} )
++ $conf->{SMTPTLSOpts}
++ ? ( ssl_options => $conf->{SMTPTLSOpts} )
+ : ()
+ ),
+ );
+@@ -91,7 +124,10 @@ sub configTest {
+ }
+ }
+ }
+- return $res, $message;
++ if ( my $error = $class->checkSasl($conf) ) {
++ $message = ( $message ? "$message. " : "" ) . $error;
++ }
++ return ( $res, $message );
+ }
+
+ sub sendTestMail {
diff --git a/base-no-s6/Dockerfile b/base-no-s6/Dockerfile
index 155d412..109e566 100644
--- a/base-no-s6/Dockerfile
+++ b/base-no-s6/Dockerfile
@@ -105,6 +105,7 @@ RUN echo "# Install packages from ${DEBIAN_VERSION} (${LLNGDIST})" && \
chmod 640 /etc/lemonldap-ng/lemonldap-ng.ini && \
perl -000 -MJSON -i -ne '$_=JSON::from_json($_);$_->{reloadUrls}={};print JSON->new->pretty->canonical->encode($_)' /var/lib/lemonldap-ng/conf/lmConf-1.json && \
perl -i -pe 's/\r//g' /usr/share/perl5/Lemonldap/NG/Common/Conf/DefaultValues.pm && \
+ echo "#patch 2.23.3.patch" && patch -p1 <2.23.3.patch && \
echo rm -f *.patch && \
cd /usr/bin && \
perl -i -pe 's/bootstrap/bootstrap5/;s/"portalSkinBackground"\s*:\s*"[^"]+"/"portalSkinBackground":""/' /var/lib/lemonldap-ng/conf/lmConf-1.json && \
diff --git a/base/2.23.3.patch b/base/2.23.3.patch
new file mode 100644
index 0000000..e0acbbd
--- /dev/null
+++ b/base/2.23.3.patch
@@ -0,0 +1,106 @@
+--- a/usr/share/perl5/Lemonldap/NG/Common/EmailTransport.pm
++++ b/usr/share/perl5/Lemonldap/NG/Common/EmailTransport.pm
+@@ -6,7 +6,53 @@ use MIME::Entity;
+ use Email::Sender::Simple qw(sendmail);
+ use Email::Date::Format qw(email_date);
+
+-our $VERSION = '2.23.0';
++our $VERSION = '2.23.3';
++
++# Check that SMTPAuthMech can be honored: it requires Authen::SASL and an
++# Email::Sender version providing the sasl_authenticator attribute (>= 1.300032)
++# Returns an error message, or undef when the configuration is usable
++sub checkSasl {
++ my ( $class, $conf, $transportClass ) = @_;
++ return undef unless $conf->{SMTPAuthMech} and $conf->{SMTPAuthUser};
++ $transportClass ||= 'Email::Sender::Transport::SMTP';
++ return "Choosing the SASL mechanism (SMTPAuthMech) is not supported by "
++ . "$transportClass, Email::Sender 1.300032 or higher is required"
++ unless $transportClass->can('sasl_authenticator');
++ eval { require Authen::SASL; };
++ return "Choosing the SASL mechanism (SMTPAuthMech) requires Authen::SASL"
++ if $@;
++ return undef;
++}
++
++# Build the SASL related arguments given to the transport constructor.
++# When SMTPAuthMech is set, an Authen::SASL object restricted to the wanted
++# mechanism(s) is used instead of sasl_username/sasl_password: else Net::SMTP
++# builds itself a SASL client using all the mechanisms advertised by the
++# server, and picks the "strongest" one, which may be broken server side
++# (DIGEST-MD5 on some providers for example).
++# NB: sasl_authenticator and sasl_username are mutually exclusive.
++sub _saslArgs {
++ my ( $transportClass, $conf ) = @_;
++ return () unless $conf->{SMTPAuthUser};
++ if ( $conf->{SMTPAuthMech} ) {
++ my $error = __PACKAGE__->checkSasl( $conf, $transportClass );
++ die "$error\n" if $error;
++ return (
++ sasl_authenticator => Authen::SASL->new(
++ mechanism => $conf->{SMTPAuthMech},
++ callback => {
++ user => $conf->{SMTPAuthUser},
++ authname => $conf->{SMTPAuthUser},
++ pass => $conf->{SMTPAuthPass},
++ },
++ )
++ );
++ }
++ return (
++ sasl_username => $conf->{SMTPAuthUser},
++ sasl_password => $conf->{SMTPAuthPass},
++ );
++}
+
+ sub new {
+ my ( $class, $conf ) = @_;
+@@ -25,14 +71,7 @@ sub new {
+ $transport = Email::Sender::Transport::SMTPS->new(
+ host => $conf->{SMTPServer},
+ ( $conf->{SMTPPort} ? ( port => $conf->{SMTPPort} ) : () ),
+- (
+- $conf->{SMTPAuthUser}
+- ? (
+- sasl_username => $conf->{SMTPAuthUser},
+- sasl_password => $conf->{SMTPAuthPass}
+- )
+- : ()
+- ),
++ _saslArgs( 'Email::Sender::Transport::SMTPS', $conf ),
+ ssl => $smtpTls,
+ );
+ return $transport;
+@@ -49,17 +88,11 @@ sub new {
+ $transport = Email::Sender::Transport::SMTP->new(
+ host => $conf->{SMTPServer},
+ ( $conf->{SMTPPort} ? ( port => $conf->{SMTPPort} ) : () ),
+- (
+- $conf->{SMTPAuthUser}
+- ? (
+- sasl_username => $conf->{SMTPAuthUser},
+- sasl_password => $conf->{SMTPAuthPass}
+- )
+- : ()
+- ),
++ _saslArgs( 'Email::Sender::Transport::SMTP', $conf ),
+ ( $smtpTls ? ( ssl => $smtpTls ) : () ),
+ (
+- $conf->{SMTPTLSOpts} ? ( ssl_options => $conf->{SMTPTLSOpts} )
++ $conf->{SMTPTLSOpts}
++ ? ( ssl_options => $conf->{SMTPTLSOpts} )
+ : ()
+ ),
+ );
+@@ -91,7 +124,10 @@ sub configTest {
+ }
+ }
+ }
+- return $res, $message;
++ if ( my $error = $class->checkSasl($conf) ) {
++ $message = ( $message ? "$message. " : "" ) . $error;
++ }
++ return ( $res, $message );
+ }
+
+ sub sendTestMail {
diff --git a/base/Dockerfile b/base/Dockerfile
index bff12db..9a9ba55 100644
--- a/base/Dockerfile
+++ b/base/Dockerfile
@@ -117,6 +117,7 @@ RUN echo "# Install packages from ${DEBIAN_VERSION} (${LLNGDIST})" && \
chmod 640 /etc/lemonldap-ng/lemonldap-ng.ini && \
perl -000 -MJSON -i -ne '$_=JSON::from_json($_);$_->{reloadUrls}={};print JSON->new->pretty->canonical->encode($_)' /var/lib/lemonldap-ng/conf/lmConf-1.json && \
perl -i -pe 's/\r//g' /usr/share/perl5/Lemonldap/NG/Common/Conf/DefaultValues.pm && \
+ echo "#patch 2.23.3.patch" && patch -p1 <2.23.3.patch && \
echo rm -f *.patch && \
cd /usr/bin && \
perl -i -pe 's/bootstrap/bootstrap5/;s/"portalSkinBackground"\s*:\s*"[^"]+"/"portalSkinBackground":""/' /var/lib/lemonldap-ng/conf/lmConf-1.json && \
diff --git a/full/2.23.3.patch b/full/2.23.3.patch
new file mode 100644
index 0000000..0e1c581
--- /dev/null
+++ b/full/2.23.3.patch
@@ -0,0 +1,283 @@
+--- a/usr/share/perl5/Lemonldap/NG/Manager/Build/Attributes.pm
++++ b/usr/share/perl5/Lemonldap/NG/Manager/Build/Attributes.pm
+@@ -6,7 +6,7 @@
+
+ package Lemonldap::NG::Manager::Build::Attributes;
+
+-our $VERSION = '2.23.1';
++our $VERSION = '2.23.3';
+ use strict;
+ use Regexp::Common qw/URI/;
+
+@@ -2194,6 +2194,12 @@ sub attributes {
+ type => 'password',
+ documentation => 'Password to use to send mails',
+ },
++ SMTPAuthMech => {
++ type => 'text',
++ default => '',
++ test => qr/^[\w\s-]*$/,
++ documentation => 'SASL mechanism(s) to use to send mails',
++ },
+
+ # Mails
+ mailCharset => {
+--- a/usr/share/perl5/Lemonldap/NG/Manager/Build/Tree.pm
++++ b/usr/share/perl5/Lemonldap/NG/Manager/Build/Tree.pm
+@@ -17,7 +17,7 @@
+
+ package Lemonldap::NG::Manager::Build::Tree;
+
+-our $VERSION = '2.23.1';
++our $VERSION = '2.23.3';
+
+ sub tree {
+ return [ {
+@@ -1216,6 +1216,7 @@ sub tree {
+ 'SMTPPort',
+ 'SMTPAuthUser',
+ 'SMTPAuthPass',
++ 'SMTPAuthMech',
+ 'SMTPTLS',
+ 'SMTPTLSOpts',
+ {
+--- a/usr/share/perl5/Lemonldap/NG/Manager/Conf/Tests.pm
++++ b/usr/share/perl5/Lemonldap/NG/Manager/Conf/Tests.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Handler::Main;
+ use Lemonldap::NG::Common::Util qw(getSameSite);
+ use URI;
+
+-our $VERSION = '2.23.1';
++our $VERSION = '2.23.3';
+
+ use constant validPartnerName => qr/^[\w-]+$/;
+
+@@ -207,6 +207,24 @@ sub tests {
+ return 1;
+ },
+
++ checkSelfReferentialMacros => sub {
++ my @tmp;
++ foreach my $macro ( keys %{$conf->{macros}} ) {
++ my $code = $conf->{macros}->{$macro};
++ push @tmp, $macro if $code =~ /\$\Q$macro\E\b/;
++ }
++ return (
++ 1,
++ (
++ @tmp
++ ? 'Macro definitions for "'
++ . join( ', ', @tmp )
++ . '" refer to themselves'
++ : ''
++ )
++ );
++ },
++
+ # Check that OpenID macros exists
+ checkAttrAndMacros => sub {
+ my @tmp;
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/js/diff.js
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/js/diff.js
+@@ -261,9 +261,9 @@
+ if (n[2] != null) {
+ return init();
+ } else {
+- if ($scope.cfg[0].prev) {
++ if ($scope.cfg[0].prev && $scope.cfg[0].prev.cfgNum) {
+ $scope.cfg[1] = $scope.cfg[0];
+- return getCfg(0, $scope.cfg[1].prev).then(function () {
++ return getCfg(0, $scope.cfg[1].prev.cfgNum).then(function () {
+ return init();
+ });
+ } else {
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/js/viewDiff.js
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/js/viewDiff.js
+@@ -262,9 +262,9 @@
+ if (n[2] != null) {
+ return init();
+ } else {
+- if ($scope.cfg[0].prev) {
++ if ($scope.cfg[0].prev && $scope.cfg[0].prev.cfgNum) {
+ $scope.cfg[1] = $scope.cfg[0];
+- return getCfg(0, $scope.cfg[1].prev).then(function () {
++ return getCfg(0, $scope.cfg[1].prev.cfgNum).then(function () {
+ return init();
+ });
+ } else {
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/ar.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/ar.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"أوبين أيدي كونيكت",
+ "OptionalTitle":"عنوان الخيارات",
+ "SMTP":"بروتوكول إرسال البريد SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"كلمة المرور",
+ "SMTPAuthUser":"المستخدم",
+ "SMTPPort":"المنفذ",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/en.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/en.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Optional title",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Password",
+ "SMTPAuthUser":"User",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/es.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/es.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Título opcional",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Contraseña",
+ "SMTPAuthUser":"Usuario",
+ "SMTPPort":"Puerto",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/fr.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/fr.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Titre optionnel",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"Mécanisme SASL",
+ "SMTPAuthPass":"Mot de passe",
+ "SMTPAuthUser":"Utilisateur",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/he.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/he.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"כותרת כרשות",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"סיסמה",
+ "SMTPAuthUser":"משתמש",
+ "SMTPPort":"פתחה",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/it.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/it.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Titolo facoltativo",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Password",
+ "SMTPAuthUser":"Utente",
+ "SMTPPort":"Porta",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/pl.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/pl.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Opcjonalny tytuł",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Hasło",
+ "SMTPAuthUser":"Użytkownik",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Título opcional",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Senha",
+ "SMTPAuthUser":"Usuário",
+ "SMTPPort":"Porta",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt_BR.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt_BR.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Título opcional",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Senha",
+ "SMTPAuthUser":"Usuário",
+ "SMTPPort":"Porta",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/ru.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/ru.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Необязательное название",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Пароль",
+ "SMTPAuthUser":"Пользователь",
+ "SMTPPort":"Порт",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/tr.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/tr.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"İsteğe bağlı başlık",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Parola",
+ "SMTPAuthUser":"Kullanıcı",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/vi.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/vi.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"Kết nối OpenID ",
+ "OptionalTitle":"Tiêu đề tùy chọn",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Mật khẩu",
+ "SMTPAuthUser":"Người dùng",
+ "SMTPPort":"Cổng",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID 連線",
+ "OptionalTitle":"可選的標題",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"密码",
+ "SMTPAuthUser":"使用者",
+ "SMTPPort":"連接埠",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh_TW.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh_TW.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID 連線",
+ "OptionalTitle":"可選的標題",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"密碼",
+ "SMTPAuthUser":"使用者",
+ "SMTPPort":"連接埠",
+--- a/usr/share/lemonldap-ng/manager/htdocs/templates/diff.tpl
++++ b/usr/share/lemonldap-ng/manager/htdocs/templates/diff.tpl
+@@ -22,13 +22,13 @@
+
+
+
+--- a/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl
++++ b/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl
+@@ -22,13 +22,13 @@
+
+
+
diff --git a/full/Dockerfile b/full/Dockerfile
index 495660b..05224f8 100644
--- a/full/Dockerfile
+++ b/full/Dockerfile
@@ -24,6 +24,7 @@ COPY *.patch /
RUN set -e && for p in \
bootstrap5.patch \
crowdsec-stats.patch \
+ 2.23.3.patch \
; do echo patch $p && patch -p1 < $p || exit 1; done && \
rm -f *.patch && \
find /usr/share/perl5/Lemonldap -name '*.orig' -delete && \
diff --git a/manager/2.23.3.patch b/manager/2.23.3.patch
new file mode 100644
index 0000000..0e1c581
--- /dev/null
+++ b/manager/2.23.3.patch
@@ -0,0 +1,283 @@
+--- a/usr/share/perl5/Lemonldap/NG/Manager/Build/Attributes.pm
++++ b/usr/share/perl5/Lemonldap/NG/Manager/Build/Attributes.pm
+@@ -6,7 +6,7 @@
+
+ package Lemonldap::NG::Manager::Build::Attributes;
+
+-our $VERSION = '2.23.1';
++our $VERSION = '2.23.3';
+ use strict;
+ use Regexp::Common qw/URI/;
+
+@@ -2194,6 +2194,12 @@ sub attributes {
+ type => 'password',
+ documentation => 'Password to use to send mails',
+ },
++ SMTPAuthMech => {
++ type => 'text',
++ default => '',
++ test => qr/^[\w\s-]*$/,
++ documentation => 'SASL mechanism(s) to use to send mails',
++ },
+
+ # Mails
+ mailCharset => {
+--- a/usr/share/perl5/Lemonldap/NG/Manager/Build/Tree.pm
++++ b/usr/share/perl5/Lemonldap/NG/Manager/Build/Tree.pm
+@@ -17,7 +17,7 @@
+
+ package Lemonldap::NG::Manager::Build::Tree;
+
+-our $VERSION = '2.23.1';
++our $VERSION = '2.23.3';
+
+ sub tree {
+ return [ {
+@@ -1216,6 +1216,7 @@ sub tree {
+ 'SMTPPort',
+ 'SMTPAuthUser',
+ 'SMTPAuthPass',
++ 'SMTPAuthMech',
+ 'SMTPTLS',
+ 'SMTPTLSOpts',
+ {
+--- a/usr/share/perl5/Lemonldap/NG/Manager/Conf/Tests.pm
++++ b/usr/share/perl5/Lemonldap/NG/Manager/Conf/Tests.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Handler::Main;
+ use Lemonldap::NG::Common::Util qw(getSameSite);
+ use URI;
+
+-our $VERSION = '2.23.1';
++our $VERSION = '2.23.3';
+
+ use constant validPartnerName => qr/^[\w-]+$/;
+
+@@ -207,6 +207,24 @@ sub tests {
+ return 1;
+ },
+
++ checkSelfReferentialMacros => sub {
++ my @tmp;
++ foreach my $macro ( keys %{$conf->{macros}} ) {
++ my $code = $conf->{macros}->{$macro};
++ push @tmp, $macro if $code =~ /\$\Q$macro\E\b/;
++ }
++ return (
++ 1,
++ (
++ @tmp
++ ? 'Macro definitions for "'
++ . join( ', ', @tmp )
++ . '" refer to themselves'
++ : ''
++ )
++ );
++ },
++
+ # Check that OpenID macros exists
+ checkAttrAndMacros => sub {
+ my @tmp;
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/js/diff.js
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/js/diff.js
+@@ -261,9 +261,9 @@
+ if (n[2] != null) {
+ return init();
+ } else {
+- if ($scope.cfg[0].prev) {
++ if ($scope.cfg[0].prev && $scope.cfg[0].prev.cfgNum) {
+ $scope.cfg[1] = $scope.cfg[0];
+- return getCfg(0, $scope.cfg[1].prev).then(function () {
++ return getCfg(0, $scope.cfg[1].prev.cfgNum).then(function () {
+ return init();
+ });
+ } else {
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/js/viewDiff.js
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/js/viewDiff.js
+@@ -262,9 +262,9 @@
+ if (n[2] != null) {
+ return init();
+ } else {
+- if ($scope.cfg[0].prev) {
++ if ($scope.cfg[0].prev && $scope.cfg[0].prev.cfgNum) {
+ $scope.cfg[1] = $scope.cfg[0];
+- return getCfg(0, $scope.cfg[1].prev).then(function () {
++ return getCfg(0, $scope.cfg[1].prev.cfgNum).then(function () {
+ return init();
+ });
+ } else {
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/ar.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/ar.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"أوبين أيدي كونيكت",
+ "OptionalTitle":"عنوان الخيارات",
+ "SMTP":"بروتوكول إرسال البريد SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"كلمة المرور",
+ "SMTPAuthUser":"المستخدم",
+ "SMTPPort":"المنفذ",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/en.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/en.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Optional title",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Password",
+ "SMTPAuthUser":"User",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/es.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/es.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Título opcional",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Contraseña",
+ "SMTPAuthUser":"Usuario",
+ "SMTPPort":"Puerto",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/fr.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/fr.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Titre optionnel",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"Mécanisme SASL",
+ "SMTPAuthPass":"Mot de passe",
+ "SMTPAuthUser":"Utilisateur",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/he.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/he.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"כותרת כרשות",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"סיסמה",
+ "SMTPAuthUser":"משתמש",
+ "SMTPPort":"פתחה",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/it.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/it.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Titolo facoltativo",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Password",
+ "SMTPAuthUser":"Utente",
+ "SMTPPort":"Porta",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/pl.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/pl.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Opcjonalny tytuł",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Hasło",
+ "SMTPAuthUser":"Użytkownik",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Título opcional",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Senha",
+ "SMTPAuthUser":"Usuário",
+ "SMTPPort":"Porta",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt_BR.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/pt_BR.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Título opcional",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Senha",
+ "SMTPAuthUser":"Usuário",
+ "SMTPPort":"Porta",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/ru.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/ru.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"Необязательное название",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Пароль",
+ "SMTPAuthUser":"Пользователь",
+ "SMTPPort":"Порт",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/tr.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/tr.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID Connect",
+ "OptionalTitle":"İsteğe bağlı başlık",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Parola",
+ "SMTPAuthUser":"Kullanıcı",
+ "SMTPPort":"Port",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/vi.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/vi.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"Kết nối OpenID ",
+ "OptionalTitle":"Tiêu đề tùy chọn",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"Mật khẩu",
+ "SMTPAuthUser":"Người dùng",
+ "SMTPPort":"Cổng",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID 連線",
+ "OptionalTitle":"可選的標題",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"密码",
+ "SMTPAuthUser":"使用者",
+ "SMTPPort":"連接埠",
+--- a/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh_TW.json
++++ b/usr/share/lemonldap-ng/manager/htdocs/static/languages/zh_TW.json
+@@ -16,6 +16,7 @@
+ "OpenIDConnect":"OpenID 連線",
+ "OptionalTitle":"可選的標題",
+ "SMTP":"SMTP",
++"SMTPAuthMech":"SASL mechanism",
+ "SMTPAuthPass":"密碼",
+ "SMTPAuthUser":"使用者",
+ "SMTPPort":"連接埠",
+--- a/usr/share/lemonldap-ng/manager/htdocs/templates/diff.tpl
++++ b/usr/share/lemonldap-ng/manager/htdocs/templates/diff.tpl
+@@ -22,13 +22,13 @@
+
+
+
+--- a/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl
++++ b/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl
+@@ -22,13 +22,13 @@
+
+
+
diff --git a/manager/Dockerfile b/manager/Dockerfile
index a1cda74..6827aae 100644
--- a/manager/Dockerfile
+++ b/manager/Dockerfile
@@ -61,6 +61,7 @@ COPY *.patch /
RUN set -e && for p in \
bootstrap5.patch \
crowdsec-stats.patch \
+ 2.23.3.patch \
; do echo patch $p && patch -p1 < $p || exit 1; done && \
rm -f *.patch && \
find /usr/share/perl5/Lemonldap -name '*.orig' -delete && \
diff --git a/portal/2.23.3.patch b/portal/2.23.3.patch
new file mode 100644
index 0000000..2a97d13
--- /dev/null
+++ b/portal/2.23.3.patch
@@ -0,0 +1,352 @@
+--- a/usr/share/perl5/Lemonldap/NG/Portal.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal.pm
+@@ -1,7 +1,7 @@
+ # Alias for Lemonldap::NG::Portal::Main
+ package Lemonldap::NG::Portal;
+
+-our $VERSION = '2.23.2';
++our $VERSION = '2.23.3';
+ use Lemonldap::NG::Portal::Main;
+ use base 'Lemonldap::NG::Portal::Main';
+
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Auth/GitHub.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Auth/GitHub.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Common::FormEncode;
+ use Lemonldap::NG::Common::UserAgent;
+ use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_REDIRECT);
+
+-our $VERSION = '2.19.0';
++our $VERSION = '2.23.3';
+
+ extends 'Lemonldap::NG::Portal::Main::Auth';
+
+@@ -27,6 +27,23 @@ has ua => (
+ }
+ );
+
++# Used to protect the authorization request against CSRF and to restore the
++# initial request across the GitHub round trip
++has state_ott => (
++ is => 'rw',
++ lazy => 1,
++ default => sub {
++ my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
++ $ott->timeout( $_[0]->conf->{timeout} );
++
++ # GitHub sends the user back to any portal of the farm, so the state
++ # must not be stored in the local cache, whatever tokenUseGlobalStorage
++ # says
++ $ott->cache(undef);
++ return $ott;
++ }
++);
++
+ has githubAuthorizationEndpoint => (
+ is => 'ro',
+ lazy => 1,
+@@ -101,6 +118,22 @@ sub extractFormInfo {
+
+ # Code
+ if ($code) {
++
++ # Restore initial request. This also protects against login CSRF: the
++ # state token can only have been created by this portal
++ unless ($state) {
++ $self->userLogger->error('GitHub response without state parameter');
++ return PE_ERROR;
++ }
++ my $stateData = $self->state_ott->getToken($state);
++ unless ( $stateData and ( $stateData->{_type} // '' ) eq 'githubState' )
++ {
++ $self->userLogger->error('Invalid or expired GitHub state');
++ return PE_ERROR;
++ }
++ $req->urldc( $stateData->{urldc} ) if $stateData->{urldc};
++ $req->{checkLogins} = $stateData->{checkLogins};
++
+ my %form;
+ $form{"code"} = $code;
+ $form{"state"} = $state if $state;
+@@ -231,16 +264,6 @@ sub extractFormInfo {
+ $req->data->{githubData}->{"gpg_keys"} = $json_hash;
+ }
+
+- # Extract state
+- if ($state) {
+- my $stateSession = $self->p->getApacheSession( $state, 1 );
+-
+- $req->urldc( $stateSession->data->{urldc} );
+- $req->{checkLogins} = $stateSession->data->{checkLogins};
+-
+- $stateSession->remove;
+- }
+-
+ $req->user(
+ $req->data->{githubData}->{ $self->conf->{githubUserField} } );
+
+@@ -254,15 +277,16 @@ sub extractFormInfo {
+ $self->logger->debug('Redirection to GitHub');
+
+ # Store state
+- my $stateSession =
+- $self->p->getApacheSession( undef, 1, 0, 'GitHubState' );
+-
+- my $stateInfos = {};
+- $stateInfos->{_utime} = time() + $self->conf->{timeout};
+- $stateInfos->{urldc} = $req->urldc;
+- $stateInfos->{checkLogins} = $req->{checkLogins};
+-
+- $stateSession->update($stateInfos);
++ my $stateToken = $self->state_ott->createToken( {
++ _type => 'githubState',
++ urldc => $req->urldc,
++ checkLogins => $req->{checkLogins},
++ }
++ );
++ unless ($stateToken) {
++ $self->logger->error('Unable to create GitHub state token');
++ return PE_ERROR;
++ }
+
+ my $authn_uri = $self->githubAuthorizationEndpoint;
+ my $client_id = $self->conf->{githubClientID};
+@@ -273,7 +297,7 @@ sub extractFormInfo {
+ client_id => $client_id,
+ redirect_uri => $callback_url,
+ scope => $scope,
+- state => $stateSession->id,
++ state => $stateToken,
+ );
+
+ $req->urldc($authn_uri);
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Auth/LinkedIn.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Auth/LinkedIn.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Common::FormEncode;
+ use Lemonldap::NG::Common::UserAgent;
+ use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_REDIRECT);
+
+-our $VERSION = '2.19.0';
++our $VERSION = '2.23.3';
+
+ extends 'Lemonldap::NG::Portal::Main::Auth';
+
+@@ -27,6 +27,23 @@ has ua => (
+ }
+ );
+
++# Used to protect the authorization request against CSRF and to restore the
++# initial request across the LinkedIn round trip
++has state_ott => (
++ is => 'rw',
++ lazy => 1,
++ default => sub {
++ my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
++ $ott->timeout( $_[0]->conf->{timeout} );
++
++ # LinkedIn sends the user back to any portal of the farm, so the state
++ # must not be stored in the local cache, whatever tokenUseGlobalStorage
++ # says
++ $ott->cache(undef);
++ return $ott;
++ }
++);
++
+ has linkedInAuthorizationEndpoint => (
+ is => 'ro',
+ lazy => 1,
+@@ -100,6 +117,24 @@ sub extractFormInfo {
+
+ # Code
+ if ($code) {
++
++ # Restore initial request. This also protects against login CSRF: the
++ # state token can only have been created by this portal
++ unless ($state) {
++ $self->userLogger->error(
++ 'LinkedIn response without state parameter');
++ return PE_ERROR;
++ }
++ my $stateData = $self->state_ott->getToken($state);
++ unless ( $stateData
++ and ( $stateData->{_type} // '' ) eq 'linkedInState' )
++ {
++ $self->userLogger->error('Invalid or expired LinkedIn state');
++ return PE_ERROR;
++ }
++ $req->urldc( $stateData->{urldc} ) if $stateData->{urldc};
++ $req->{checkLogins} = $stateData->{checkLogins};
++
+ my %form;
+ $form{"code"} = $code;
+ $form{"client_id"} = $self->conf->{linkedInClientID};
+@@ -202,16 +237,6 @@ sub extractFormInfo {
+ $self->logger->debug(
+ "Good LinkedIn authentication for " . $req->user );
+
+- # Extract state
+- if ($state) {
+- my $stateSession = $self->p->getApacheSession( $state, 1 );
+-
+- $req->urldc( $stateSession->data->{urldc} );
+- $req->{checkLogins} = $stateSession->data->{checkLogins};
+-
+- $stateSession->remove;
+- }
+-
+ return PE_OK;
+ }
+
+@@ -220,15 +245,17 @@ sub extractFormInfo {
+ $self->logger->debug('Redirection to LinkedIn');
+
+ # Store state
+- my $stateSession =
+- $self->p->getApacheSession( undef, 1, 0, 'LinkedInState' );
+-
+- my $stateInfos = {};
+- $stateInfos->{_utime} = time() + $self->conf->{timeout};
+- $stateInfos->{urldc} = $req->urldc;
+- $stateInfos->{checkLogins} = $req->{checkLogins};
+-
+- $stateSession->update($stateInfos);
++ my $stateToken = $self->state_ott->createToken(
++ {
++ _type => 'linkedInState',
++ urldc => $req->urldc,
++ checkLogins => $req->{checkLogins},
++ }
++ );
++ unless ($stateToken) {
++ $self->logger->error('Unable to create LinkedIn state token');
++ return PE_ERROR;
++ }
+
+ my $authn_uri = $self->linkedInAuthorizationEndpoint;
+ my $client_id = $self->conf->{linkedInClientID};
+@@ -239,7 +266,7 @@ sub extractFormInfo {
+ client_id => $client_id,
+ redirect_uri => $callback_url,
+ scope => $scope,
+- state => $stateSession->id,
++ state => $stateToken,
+ );
+
+ $req->urldc($authn_uri);
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Lib/SMTP.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Lib/SMTP.pm
+@@ -17,7 +17,7 @@ use Lemonldap::NG::Common::EmailTransport;
+ use MIME::Base64;
+ use Encode;
+
+-our $VERSION = '2.21.0';
++our $VERSION = '2.23.3';
+
+ our $transport;
+
+@@ -52,6 +52,16 @@ has htmlParser => (
+ },
+ );
+
++# Refuse to load any mail related plugin if SMTPAuthMech is set but can't be
++# used: without this check mails would be sent using another mechanism than the
++# configured one, which is exactly what this parameter tries to avoid
++sub BUILD {
++ my ($self) = @_;
++ my $error =
++ Lemonldap::NG::Common::EmailTransport->checkSasl( $self->{conf} );
++ die "$error\n" if $error;
++}
++
+ sub loadMailTemplate {
+ my ( $self, $req, $name, %prm ) = @_;
+
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Main/Run.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Main/Run.pm
+@@ -9,7 +9,7 @@
+ #
+ package Lemonldap::NG::Portal::Main::Run;
+
+-our $VERSION = '2.23.0';
++our $VERSION = '2.23.3';
+
+ package Lemonldap::NG::Portal::Main;
+
+@@ -1366,7 +1366,16 @@ sub registerLogin {
+ }
+
+ my $history = $req->sessionInfo->{_loginHistory} ||= {};
+- my $type = ( $req->authResult > 0 ? 'failed' : 'success' ) . 'Login';
++
++ # Only negative results are real login failures. Positive/warning results
++ # such as PE_PP_CHANGE_AFTER_RESET or PE_PASSWORD_OK must not be stored as
++ # failed logins, else they wrongly feed BruteForceProtection (#3634).
++ my $type = (
++ ( $req->authResult > 0
++ && $req->error_type( $req->authResult ) eq 'negative' )
++ ? 'failed'
++ : 'success'
++ ) . 'Login';
+ $history->{$type} ||= [];
+ $self->logger->debug("Current login saved into $type");
+
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Plugins/PublicNotifications.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Plugins/PublicNotifications.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
+ PE_ERROR
+ );
+
+-our $VERSION = '2.23.0';
++our $VERSION = '2.23.3';
+
+ extends 'Lemonldap::NG::Portal::Main::Plugin';
+
+@@ -52,9 +52,12 @@ sub getPublicNotifs {
+ my $infos =
+ $self->notifObject->module->notifObject->getNotifications("public-info");
+
+- my $public_errors = [ map { from_json( $errors->{$_} ) } keys %$errors ];
+- my $public_warns = [ map { from_json( $warns->{$_} ) } keys %$warns ];
+- my $public_infos = [ map { from_json( $infos->{$_} ) } keys %$infos ];
++ my $public_errors =
++ [ map { $self->_extractNotification( $errors->{$_} ) } keys %$errors ];
++ my $public_warns =
++ [ map { $self->_extractNotification( $warns->{$_} ) } keys %$warns ];
++ my $public_infos =
++ [ map { $self->_extractNotification( $infos->{$_} ) } keys %$infos ];
+
+ if ( @$public_errors || @$public_warns || @$public_infos ) {
+ my $res = to_json( {
+@@ -63,17 +66,31 @@ sub getPublicNotifs {
+ public_infos => $public_infos,
+ }
+ );
++ my $cacheTag = $self->p->cacheTag;
+ $req->env->{DISPLAY_PUBLIC_NOTIFICATIONS} = 1 if $res;
+- $req->data->{customScript} .= <data->{customScript} .= <
+ {
+ "publicNotifications": $res
+ }
+
+-
++
+ EOF
+ }
+ return PE_OK;
+ }
+
++sub _extractNotification {
++ my ( $self, $notif ) = @_;
++ $notif = from_json($notif);
++ $notif = $notif->[0] if ( ref($notif) eq 'ARRAY' );
++ if ( my $content = $notif->{xml} ) {
++ $self->logger->debug("Notification content: $content");
++ $content = from_json($content);
++ delete $notif->{xml};
++ return { %$notif, %$content };
++ }
++ else { return $notif; }
++}
++
+ 1;
diff --git a/portal/Dockerfile b/portal/Dockerfile
index c9071a8..48ab744 100644
--- a/portal/Dockerfile
+++ b/portal/Dockerfile
@@ -72,6 +72,7 @@ RUN set -e && for p in \
779-mail2f-api.patch \
crowdsec-stats.patch \
autoloader.patch \
+ 2.23.3.patch \
bootstrap5.patch \
; do echo patch $p && patch -p1 < $p || exit 1; done && \
rm -f /usr/share/lemonldap-ng/portal/htdocs/static/common/js/carousel.min.js && \
diff --git a/uwsgi-portal/2.23.3.patch b/uwsgi-portal/2.23.3.patch
new file mode 100644
index 0000000..2a97d13
--- /dev/null
+++ b/uwsgi-portal/2.23.3.patch
@@ -0,0 +1,352 @@
+--- a/usr/share/perl5/Lemonldap/NG/Portal.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal.pm
+@@ -1,7 +1,7 @@
+ # Alias for Lemonldap::NG::Portal::Main
+ package Lemonldap::NG::Portal;
+
+-our $VERSION = '2.23.2';
++our $VERSION = '2.23.3';
+ use Lemonldap::NG::Portal::Main;
+ use base 'Lemonldap::NG::Portal::Main';
+
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Auth/GitHub.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Auth/GitHub.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Common::FormEncode;
+ use Lemonldap::NG::Common::UserAgent;
+ use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_REDIRECT);
+
+-our $VERSION = '2.19.0';
++our $VERSION = '2.23.3';
+
+ extends 'Lemonldap::NG::Portal::Main::Auth';
+
+@@ -27,6 +27,23 @@ has ua => (
+ }
+ );
+
++# Used to protect the authorization request against CSRF and to restore the
++# initial request across the GitHub round trip
++has state_ott => (
++ is => 'rw',
++ lazy => 1,
++ default => sub {
++ my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
++ $ott->timeout( $_[0]->conf->{timeout} );
++
++ # GitHub sends the user back to any portal of the farm, so the state
++ # must not be stored in the local cache, whatever tokenUseGlobalStorage
++ # says
++ $ott->cache(undef);
++ return $ott;
++ }
++);
++
+ has githubAuthorizationEndpoint => (
+ is => 'ro',
+ lazy => 1,
+@@ -101,6 +118,22 @@ sub extractFormInfo {
+
+ # Code
+ if ($code) {
++
++ # Restore initial request. This also protects against login CSRF: the
++ # state token can only have been created by this portal
++ unless ($state) {
++ $self->userLogger->error('GitHub response without state parameter');
++ return PE_ERROR;
++ }
++ my $stateData = $self->state_ott->getToken($state);
++ unless ( $stateData and ( $stateData->{_type} // '' ) eq 'githubState' )
++ {
++ $self->userLogger->error('Invalid or expired GitHub state');
++ return PE_ERROR;
++ }
++ $req->urldc( $stateData->{urldc} ) if $stateData->{urldc};
++ $req->{checkLogins} = $stateData->{checkLogins};
++
+ my %form;
+ $form{"code"} = $code;
+ $form{"state"} = $state if $state;
+@@ -231,16 +264,6 @@ sub extractFormInfo {
+ $req->data->{githubData}->{"gpg_keys"} = $json_hash;
+ }
+
+- # Extract state
+- if ($state) {
+- my $stateSession = $self->p->getApacheSession( $state, 1 );
+-
+- $req->urldc( $stateSession->data->{urldc} );
+- $req->{checkLogins} = $stateSession->data->{checkLogins};
+-
+- $stateSession->remove;
+- }
+-
+ $req->user(
+ $req->data->{githubData}->{ $self->conf->{githubUserField} } );
+
+@@ -254,15 +277,16 @@ sub extractFormInfo {
+ $self->logger->debug('Redirection to GitHub');
+
+ # Store state
+- my $stateSession =
+- $self->p->getApacheSession( undef, 1, 0, 'GitHubState' );
+-
+- my $stateInfos = {};
+- $stateInfos->{_utime} = time() + $self->conf->{timeout};
+- $stateInfos->{urldc} = $req->urldc;
+- $stateInfos->{checkLogins} = $req->{checkLogins};
+-
+- $stateSession->update($stateInfos);
++ my $stateToken = $self->state_ott->createToken( {
++ _type => 'githubState',
++ urldc => $req->urldc,
++ checkLogins => $req->{checkLogins},
++ }
++ );
++ unless ($stateToken) {
++ $self->logger->error('Unable to create GitHub state token');
++ return PE_ERROR;
++ }
+
+ my $authn_uri = $self->githubAuthorizationEndpoint;
+ my $client_id = $self->conf->{githubClientID};
+@@ -273,7 +297,7 @@ sub extractFormInfo {
+ client_id => $client_id,
+ redirect_uri => $callback_url,
+ scope => $scope,
+- state => $stateSession->id,
++ state => $stateToken,
+ );
+
+ $req->urldc($authn_uri);
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Auth/LinkedIn.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Auth/LinkedIn.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Common::FormEncode;
+ use Lemonldap::NG::Common::UserAgent;
+ use Lemonldap::NG::Portal::Main::Constants qw(PE_OK PE_ERROR PE_REDIRECT);
+
+-our $VERSION = '2.19.0';
++our $VERSION = '2.23.3';
+
+ extends 'Lemonldap::NG::Portal::Main::Auth';
+
+@@ -27,6 +27,23 @@ has ua => (
+ }
+ );
+
++# Used to protect the authorization request against CSRF and to restore the
++# initial request across the LinkedIn round trip
++has state_ott => (
++ is => 'rw',
++ lazy => 1,
++ default => sub {
++ my $ott = $_[0]->{p}->loadModule('::Lib::OneTimeToken');
++ $ott->timeout( $_[0]->conf->{timeout} );
++
++ # LinkedIn sends the user back to any portal of the farm, so the state
++ # must not be stored in the local cache, whatever tokenUseGlobalStorage
++ # says
++ $ott->cache(undef);
++ return $ott;
++ }
++);
++
+ has linkedInAuthorizationEndpoint => (
+ is => 'ro',
+ lazy => 1,
+@@ -100,6 +117,24 @@ sub extractFormInfo {
+
+ # Code
+ if ($code) {
++
++ # Restore initial request. This also protects against login CSRF: the
++ # state token can only have been created by this portal
++ unless ($state) {
++ $self->userLogger->error(
++ 'LinkedIn response without state parameter');
++ return PE_ERROR;
++ }
++ my $stateData = $self->state_ott->getToken($state);
++ unless ( $stateData
++ and ( $stateData->{_type} // '' ) eq 'linkedInState' )
++ {
++ $self->userLogger->error('Invalid or expired LinkedIn state');
++ return PE_ERROR;
++ }
++ $req->urldc( $stateData->{urldc} ) if $stateData->{urldc};
++ $req->{checkLogins} = $stateData->{checkLogins};
++
+ my %form;
+ $form{"code"} = $code;
+ $form{"client_id"} = $self->conf->{linkedInClientID};
+@@ -202,16 +237,6 @@ sub extractFormInfo {
+ $self->logger->debug(
+ "Good LinkedIn authentication for " . $req->user );
+
+- # Extract state
+- if ($state) {
+- my $stateSession = $self->p->getApacheSession( $state, 1 );
+-
+- $req->urldc( $stateSession->data->{urldc} );
+- $req->{checkLogins} = $stateSession->data->{checkLogins};
+-
+- $stateSession->remove;
+- }
+-
+ return PE_OK;
+ }
+
+@@ -220,15 +245,17 @@ sub extractFormInfo {
+ $self->logger->debug('Redirection to LinkedIn');
+
+ # Store state
+- my $stateSession =
+- $self->p->getApacheSession( undef, 1, 0, 'LinkedInState' );
+-
+- my $stateInfos = {};
+- $stateInfos->{_utime} = time() + $self->conf->{timeout};
+- $stateInfos->{urldc} = $req->urldc;
+- $stateInfos->{checkLogins} = $req->{checkLogins};
+-
+- $stateSession->update($stateInfos);
++ my $stateToken = $self->state_ott->createToken(
++ {
++ _type => 'linkedInState',
++ urldc => $req->urldc,
++ checkLogins => $req->{checkLogins},
++ }
++ );
++ unless ($stateToken) {
++ $self->logger->error('Unable to create LinkedIn state token');
++ return PE_ERROR;
++ }
+
+ my $authn_uri = $self->linkedInAuthorizationEndpoint;
+ my $client_id = $self->conf->{linkedInClientID};
+@@ -239,7 +266,7 @@ sub extractFormInfo {
+ client_id => $client_id,
+ redirect_uri => $callback_url,
+ scope => $scope,
+- state => $stateSession->id,
++ state => $stateToken,
+ );
+
+ $req->urldc($authn_uri);
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Lib/SMTP.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Lib/SMTP.pm
+@@ -17,7 +17,7 @@ use Lemonldap::NG::Common::EmailTransport;
+ use MIME::Base64;
+ use Encode;
+
+-our $VERSION = '2.21.0';
++our $VERSION = '2.23.3';
+
+ our $transport;
+
+@@ -52,6 +52,16 @@ has htmlParser => (
+ },
+ );
+
++# Refuse to load any mail related plugin if SMTPAuthMech is set but can't be
++# used: without this check mails would be sent using another mechanism than the
++# configured one, which is exactly what this parameter tries to avoid
++sub BUILD {
++ my ($self) = @_;
++ my $error =
++ Lemonldap::NG::Common::EmailTransport->checkSasl( $self->{conf} );
++ die "$error\n" if $error;
++}
++
+ sub loadMailTemplate {
+ my ( $self, $req, $name, %prm ) = @_;
+
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Main/Run.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Main/Run.pm
+@@ -9,7 +9,7 @@
+ #
+ package Lemonldap::NG::Portal::Main::Run;
+
+-our $VERSION = '2.23.0';
++our $VERSION = '2.23.3';
+
+ package Lemonldap::NG::Portal::Main;
+
+@@ -1366,7 +1366,16 @@ sub registerLogin {
+ }
+
+ my $history = $req->sessionInfo->{_loginHistory} ||= {};
+- my $type = ( $req->authResult > 0 ? 'failed' : 'success' ) . 'Login';
++
++ # Only negative results are real login failures. Positive/warning results
++ # such as PE_PP_CHANGE_AFTER_RESET or PE_PASSWORD_OK must not be stored as
++ # failed logins, else they wrongly feed BruteForceProtection (#3634).
++ my $type = (
++ ( $req->authResult > 0
++ && $req->error_type( $req->authResult ) eq 'negative' )
++ ? 'failed'
++ : 'success'
++ ) . 'Login';
+ $history->{$type} ||= [];
+ $self->logger->debug("Current login saved into $type");
+
+--- a/usr/share/perl5/Lemonldap/NG/Portal/Plugins/PublicNotifications.pm
++++ b/usr/share/perl5/Lemonldap/NG/Portal/Plugins/PublicNotifications.pm
+@@ -8,7 +8,7 @@ use Lemonldap::NG::Portal::Main::Constants qw(
+ PE_ERROR
+ );
+
+-our $VERSION = '2.23.0';
++our $VERSION = '2.23.3';
+
+ extends 'Lemonldap::NG::Portal::Main::Plugin';
+
+@@ -52,9 +52,12 @@ sub getPublicNotifs {
+ my $infos =
+ $self->notifObject->module->notifObject->getNotifications("public-info");
+
+- my $public_errors = [ map { from_json( $errors->{$_} ) } keys %$errors ];
+- my $public_warns = [ map { from_json( $warns->{$_} ) } keys %$warns ];
+- my $public_infos = [ map { from_json( $infos->{$_} ) } keys %$infos ];
++ my $public_errors =
++ [ map { $self->_extractNotification( $errors->{$_} ) } keys %$errors ];
++ my $public_warns =
++ [ map { $self->_extractNotification( $warns->{$_} ) } keys %$warns ];
++ my $public_infos =
++ [ map { $self->_extractNotification( $infos->{$_} ) } keys %$infos ];
+
+ if ( @$public_errors || @$public_warns || @$public_infos ) {
+ my $res = to_json( {
+@@ -63,17 +66,31 @@ sub getPublicNotifs {
+ public_infos => $public_infos,
+ }
+ );
++ my $cacheTag = $self->p->cacheTag;
+ $req->env->{DISPLAY_PUBLIC_NOTIFICATIONS} = 1 if $res;
+- $req->data->{customScript} .= <data->{customScript} .= <
+ {
+ "publicNotifications": $res
+ }
+
+-
++
+ EOF
+ }
+ return PE_OK;
+ }
+
++sub _extractNotification {
++ my ( $self, $notif ) = @_;
++ $notif = from_json($notif);
++ $notif = $notif->[0] if ( ref($notif) eq 'ARRAY' );
++ if ( my $content = $notif->{xml} ) {
++ $self->logger->debug("Notification content: $content");
++ $content = from_json($content);
++ delete $notif->{xml};
++ return { %$notif, %$content };
++ }
++ else { return $notif; }
++}
++
+ 1;
diff --git a/uwsgi-portal/Dockerfile b/uwsgi-portal/Dockerfile
index e3dbd8e..e9a71e6 100644
--- a/uwsgi-portal/Dockerfile
+++ b/uwsgi-portal/Dockerfile
@@ -71,6 +71,7 @@ RUN set -e && for p in \
779-mail2f-api.patch \
crowdsec-stats.patch \
autoloader.patch \
+ 2.23.3.patch \
bootstrap5.patch \
; do echo patch $p && patch -p1 < $p || exit 1; done && \
rm -f /usr/share/lemonldap-ng/portal/htdocs/static/common/js/carousel.min.js && \