Auth: Unterstützung für multiple Authentifizierungsbackends
authorMoritz Bunkus <m.bunkus@linet-services.de>
Mon, 29 Apr 2019 13:54:30 +0000 (15:54 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Fri, 23 Aug 2019 09:18:33 +0000 (11:18 +0200)
Über den Parameter "module" kann man nun multiple Backends angeben,
die nacheinander versucht werden, bis ein Erfolg gemeldet wird oder
die Liste durchlaufen wurde.

Zusätzlich kann man LDAP-Module mehrfach angeben. Damit
unterschiedliche Konfigurationen für jede Modulinstanz benutzt werden
können, wurde die Syntax erweitert: für "LDAP:Config-Abschnitts-Name"
wird "[authentication/Config-Abschnitts-Name]" benutzt. Zwecks
Rückwärtskompatibilität sucht "LDAP" ohne Angabe eines Namens nach dem
bisher auch verwendeten Abschnitt "[authentication/ldap]".

Nützlich ist das Ganze z.B., um einen LDAP-Fallback-Server angeben zu
können, der benutzt wird, wenn der Hauptserver nicht erreichbar sein
sollte.

SL/Auth.pm
SL/Auth/LDAP.pm
config/kivitendo.conf.default
locale/de/all

index 830e7fb..2898222 100644 (file)
@@ -5,7 +5,7 @@ use DBI;
 use Digest::MD5 qw(md5_hex);
 use IO::File;
 use Time::HiRes qw(gettimeofday);
-use List::MoreUtils qw(uniq);
+use List::MoreUtils qw(any uniq);
 use YAML;
 use Regexp::IPv6 qw($IPv6_re);
 
@@ -72,7 +72,7 @@ sub reset {
     delete $self->{column_information};
   }
 
-  $self->{authenticator}->reset;
+  $_->reset for @{ $self->{authenticators} };
 
   $self->client(undef);
 }
@@ -145,16 +145,31 @@ sub _read_auth_config {
     $self->{DB_config}   = $::lx_office_conf{'authentication/database'};
   }
 
-  if ($self->{module} eq 'DB') {
-    $self->{authenticator} = SL::Auth::DB->new($self);
+  $self->{authenticators} =  [];
+  $self->{module}       ||=  'DB';
+  $self->{module}         =~ s{^ +| +$}{}g;
 
-  } elsif ($self->{module} eq 'LDAP') {
-    $self->{authenticator} = SL::Auth::LDAP->new($::lx_office_conf{'authentication/ldap'});
-  }
+  foreach my $module (split m{ +}, $self->{module}) {
+    my $config_name;
+    ($module, $config_name) = split m{:}, $module, 2;
+    $config_name          ||= $module eq 'DB' ? 'database' : lc($module);
+    my $config              = $::lx_office_conf{'authentication/' . $config_name};
 
-  if (!$self->{authenticator}) {
-    my $locale = Locale->new('en');
-    $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
+    if (!$config) {
+      my $locale = Locale->new('en');
+      $self->mini_error($locale->text('Missing configuration section "authentication/#1" in "config/kivitendo.conf".', $config_name));
+    }
+
+    if ($module eq 'DB') {
+      push @{ $self->{authenticators} }, SL::Auth::DB->new($self);
+
+    } elsif ($module eq 'LDAP') {
+      push @{ $self->{authenticators} }, SL::Auth::LDAP->new($config);
+
+    } else {
+      my $locale = Locale->new('en');
+      $self->mini_error($locale->text('Unknown authenticantion module #1 specified in "config/kivitendo.conf".', $module));
+    }
   }
 
   my $cfg = $self->{DB_config};
@@ -169,7 +184,7 @@ sub _read_auth_config {
     $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
   }
 
-  $self->{authenticator}->verify_config();
+  $_->verify_config for @{ $self->{authenticators} };
 
   $self->{session_timeout} *= 1;
   $self->{session_timeout}  = 8 * 60 if (!$self->{session_timeout});
@@ -229,7 +244,14 @@ sub authenticate {
     return ERR_PASSWORD;
   }
 
-  my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
+  my $result = ERR_USER;
+  if ($login) {
+    foreach my $authenticator (@{ $self->{authenticators} }) {
+      $result = $authenticator->authenticate($login, $password);
+      last if $result == OK;
+    }
+  }
+
   $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
   return $result;
 }
@@ -414,15 +436,22 @@ sub save_user {
 sub can_change_password {
   my $self = shift;
 
-  return $self->{authenticator}->can_change_password();
+  return any { $_->can_change_password } @{ $self->{authenticators} };
 }
 
 sub change_password {
   my ($self, $login, $new_password) = @_;
 
-  my $result = $self->{authenticator}->change_password($login, $new_password);
+  my $overall_result = OK;
 
-  return $result;
+  foreach my $authenticator (@{ $self->{authenticators} }) {
+    next unless $authenticator->can_change_password;
+
+    my $result = $authenticator->change_password($login, $new_password);
+    $overall_result = $result if $result != OK;
+  }
+
+  return $overall_result;
 }
 
 sub read_all_users {
index b42bf87..2f651b3 100644 (file)
@@ -32,27 +32,32 @@ sub _connect {
 
   return $self->{ldap} if $self->{ldap};
 
-  my $port      = $cfg->{port} || 389;
-  $self->{ldap} = Net::LDAP->new($cfg->{host}, 'port' => $port);
+  my $port = $cfg->{port} || 389;
+  my $ldap = Net::LDAP->new($cfg->{host}, port => $port, timeout => $cfg->{timeout} || 10);
 
-  if (!$self->{ldap}) {
-    $main::form->error($main::locale->text('The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.', $cfg->{host}, $port));
+  if (!$ldap) {
+    $::lxdebug->warn($main::locale->text('The LDAP server "#1:#2" is unreachable. Please check config/kivitendo.conf.', $cfg->{host}, $port));
+    return undef;
   }
 
   if ($cfg->{tls}) {
-    my $mesg = $self->{ldap}->start_tls('verify' => 'none');
+    my $mesg = $ldap->start_tls(verify => $cfg->{verify} // 'require');
     if ($mesg->is_error()) {
-      $main::form->error($main::locale->text('The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.'));
+      $::lxdebug->warn($main::locale->text('The connection to the LDAP server cannot be encrypted (SSL/TLS startup failure). Please check config/kivitendo.conf.'));
+      return undef;
     }
   }
 
   if ($cfg->{bind_dn}) {
-    my $mesg = $self->{ldap}->bind($cfg->{bind_dn}, 'password' => $cfg->{bind_password});
+    my $mesg = $ldap->bind($cfg->{bind_dn}, 'password' => $cfg->{bind_password});
     if ($mesg->is_error()) {
-      $main::form->error($main::locale->text('Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.', $cfg->{bind_dn}));
+      $::lxdebug->warn($main::locale->text('Binding to the LDAP server as "#1" failed. Please check config/kivitendo.conf.', $cfg->{bind_dn}));
+      return undef;
     }
   }
 
+  $self->{ldap} = $ldap;
+
   return $self->{ldap};
 }
 
index 0d88cfc..cf29984 100644 (file)
@@ -4,9 +4,16 @@
 # interface.
 admin_password = admin123
 
-# Which module to use for authentication. Valid values are 'DB' and
-# 'LDAP'.  If 'LDAP' is used then users cannot change their password
-# via kivitendo.
+# Which modules to use for authentication. Valid values are 'DB' and
+# 'LDAP'. You can use multiple modules separated by spaces.
+#
+# Multiple LDAP modules with different configurations can be used by
+# postfixing 'LDAP' with the name of the configuration section to use:
+# 'LDAP:ldap_fallback' would use the data from
+# '[authentication/ldap_fallback]'. The name defaults to 'ldap' if it
+# isn't given.
+#
+# Note that the LDAP module doesn't support changing the password.
 module = DB
 
 # The cookie name can be changed if desired.
@@ -43,6 +50,8 @@ password =
 # specified.
 #
 # tls:       Activate encryption via TLS
+# verify:    If 'tls' is used, how to verify the server's certificate.
+#            Can be one of 'require' or 'none'.
 # attribute: Name of the LDAP attribute containing the user's login name
 # base_dn:   Base DN the LDAP searches start from
 # filter:    An optional LDAP filter specification. The string '<%login%>'
@@ -51,6 +60,12 @@ password =
 #            If searching the LDAP tree requires user credentials
 #            (e.g. ActiveDirectory) then these two parameters specify
 #            the user name and password to use.
+# timeout:   Timeout when connecting to the server in seconds.
+#
+# You can specify a fallback LDAP server to use in case the main one
+# isn't reachable by duplicating this whole section as
+# "[authentication/ldap_fallback]".
+#
 host          = localhost
 port          = 389
 tls           = 0
@@ -59,6 +74,8 @@ base_dn       =
 filter        =
 bind_dn       =
 bind_password =
+timeout       = 10
+verify        = require
 
 [system]
 # Set language for login and admin forms. Currently "de" (German)
index 27e76fb..d1c69dd 100755 (executable)
@@ -1907,6 +1907,7 @@ $self->{texts} = {
   'Missing Method!'             => 'Fehlender Voranmeldungszeitraum',
   'Missing Tax Authoritys Preferences' => 'Fehlende Angaben zum Finanzamt!',
   'Missing amount'              => 'Fehlbetrag',
+  'Missing configuration section "authentication/#1" in "config/kivitendo.conf".' => 'Fehlender Konfigurationsabschnitt "authentication/#1" in "config/kivitendo.conf".',
   'Missing parameter #1 in call to sub #2.' => 'Fehlender Parameter \'#1\' in Funktionsaufruf \'#2\'.',
   'Missing parameter (at least one of #1) in call to sub #2.' => 'Fehlernder Parameter (mindestens einer aus \'#1\') in Funktionsaufruf \'#2\'.',
   'Missing parameter for WebDAV file copy' => 'Fehlender Parameter für WebDAV Datei kopieren',
@@ -2018,7 +2019,6 @@ $self->{texts} = {
   'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.',
   'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.',
   'No invoices have been selected.' => 'Es wurden keine Rechnungen ausgewählt.',
-  'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.',
   'No part was selected.'       => 'Es wurde kein Artikel ausgewählt',
   'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.',
   'No picture has been uploaded' => 'Es wurde kein Bild hochgeladen',
@@ -3711,6 +3711,7 @@ $self->{texts} = {
   'Units that have already been used (e.g. for parts and services or in invoices or warehouse transactions) cannot be changed.' => 'Einheiten, die bereits in Benutzung sind (z.B. bei einer Warendefinition, einer Rechnung oder bei einer Lagerbuchung) können nachträglich nicht mehr verändert werden.',
   'Unknown Category'            => 'Unbekannte Kategorie',
   'Unknown Link'                => 'Unbekannte Verknüpfung',
+  'Unknown authenticantion module #1 specified in "config/kivitendo.conf".' => 'Unbekanntes Authentifizierungsmodul #1 angegeben in "config/kivitendo.conf".',
   'Unknown control fields: #1'  => 'Unbekannte Kontrollfelder: #1',
   'Unknown dependency \'%s\'.'  => 'Unbekannte Abhängigkeit \'%s\'.',
   'Unknown module: #1'          => 'Unbekanntes Modul #1',