From f1d8eb63ee969c9aaf5fc87b57070e1783d75dca Mon Sep 17 00:00:00 2001 From: Yadd Date: Fri, 14 Aug 2026 07:09:43 +0200 Subject: [PATCH] Import 2.23.3 --- base-no-s6/2.23.3.patch | 106 ++++++++++++ base-no-s6/Dockerfile | 1 + base/2.23.3.patch | 106 ++++++++++++ base/Dockerfile | 1 + full/2.23.3.patch | 283 ++++++++++++++++++++++++++++++ full/Dockerfile | 1 + manager/2.23.3.patch | 283 ++++++++++++++++++++++++++++++ manager/Dockerfile | 1 + portal/2.23.3.patch | 352 ++++++++++++++++++++++++++++++++++++++ portal/Dockerfile | 1 + uwsgi-portal/2.23.3.patch | 352 ++++++++++++++++++++++++++++++++++++++ uwsgi-portal/Dockerfile | 1 + 12 files changed, 1488 insertions(+) create mode 100644 base-no-s6/2.23.3.patch create mode 100644 base/2.23.3.patch create mode 100644 full/2.23.3.patch create mode 100644 manager/2.23.3.patch create mode 100644 portal/2.23.3.patch create mode 100644 uwsgi-portal/2.23.3.patch 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 @@ + +
+
+- ++ + 1 + + 2 + + +- ++ +
+
+ +--- a/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl ++++ b/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl +@@ -22,13 +22,13 @@ + +
+
+- ++ + 1 + + 2 + + +- ++ +
+
+
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 @@ + +
+
+- ++ + 1 + + 2 + + +- ++ +
+
+
+--- a/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl ++++ b/usr/share/lemonldap-ng/manager/htdocs/templates/viewDiff.tpl +@@ -22,13 +22,13 @@ + +
+
+- ++ + 1 + + 2 + + +- ++ +
+
+
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 && \