commit f30f0cce6468f895c7d59f3aab6e2a5731fc9705
Author: Moritz Bunkus <m.bunkus@linet.de>
Date:   Tue Oct 10 11:19:48 2017 +0200

    Datenbankupgrades: Unterstützung für einzelne Updates mit Superuser-Rechten
    
    Setzt man den neuen Tag `@superuser_privileges` auf 1, so schaut
    kivitendo vor dem Einspielen aller Upgrades nach, ob der konfigurierte
    Datenbankuser Superuser-Rechte hat. Falls nicht, wird die Benutzer*in
    nach entsprechenden Logindaten gefragt.
    
    Nur die Upgradescripte, die tatsächlich Superuser-Rechte benötigen,
    werden mit diesem User ausgeführt, alle anderen mit den normalen
    Rechten, um Besitzer-Wildwuchs bei neu angelegten Objekten zu
    vermeiden.

diff --git a/SL/DBUpgrade2.pm b/SL/DBUpgrade2.pm
index b47e84ebb..3918ad40d 100644
--- a/SL/DBUpgrade2.pm
+++ b/SL/DBUpgrade2.pm
@@ -100,7 +100,7 @@ sub parse_dbupdate_controls {
 
     delete @{$control}{qw(depth applied)};
 
-    my @unknown_keys = grep { !m{^ (?: depends | description | file | ignore | locales | may_fail | priority | tag ) $}x } keys %{ $control };
+    my @unknown_keys = grep { !m{^ (?: depends | description | file | ignore | locales | may_fail | priority | superuser_privileges | tag ) $}x } keys %{ $control };
     if (@unknown_keys) {
       _control_error($form, $file_name, sprintf($locale->text("Unknown control fields: #1", join(' ', sort({ lc $a cmp lc $b } @unknown_keys)))));
     }
diff --git a/SL/User.pm b/SL/User.pm
index f324e3629..a271e77df 100644
--- a/SL/User.pm
+++ b/SL/User.pm
@@ -36,7 +36,7 @@
 package User;
 
 use IO::File;
-use Fcntl qw(:seek);
+use List::MoreUtils qw(any);
 
 use SL::DB;
 #use SL::Auth;
@@ -103,6 +103,42 @@ sub country_codes {
   return %cc;
 }
 
+sub _handle_superuser_privileges {
+  my ($self, $form) = @_;
+
+  if ($form->{database_superuser_username}) {
+    $::auth->set_session_value("database_superuser_username" => $form->{database_superuser_username}, "database_superuser_password" => $form->{database_superuser_password});
+  }
+
+  my %dbconnect_form          = %{ $form };
+  my ($su_user, $su_password) = map { $::auth->get_session_value("database_superuser_$_") } qw(username password);
+
+  if ($su_user) {
+    $dbconnect_form{dbuser}   = $su_user;
+    $dbconnect_form{dbpasswd} = $su_password;
+  }
+
+  dbconnect_vars(\%dbconnect_form, $form->{dbname});
+
+  my %result = (
+    username => $dbconnect_form{dbuser},
+    password => $dbconnect_form{dbpasswd},
+  );
+
+  $::auth->set_session_value("database_superuser_username" => $dbconnect_form{dbuser}, "database_superuser_password" => $dbconnect_form{dbpasswd});
+
+  my $dbh = SL::DBConnect->connect($dbconnect_form{dbconnect}, $dbconnect_form{dbuser}, $dbconnect_form{dbpasswd}, SL::DBConnect->get_options);
+  return (%result, error => $::locale->text('The credentials (username & password) for connecting database are wrong.')) if !$dbh;
+
+  my ($is_superuser) = $dbh->selectrow_array(qq|SELECT usesuper FROM pg_user WHERE usename = ?|, undef, $dbconnect_form{dbuser});
+
+  $dbh->disconnect;
+
+  return (%result, have_privileges => 1) if $is_superuser;
+  return (%result)                       if !$su_user; # no error message if credentials weren't set by the user
+  return (%result, error => $::locale->text('The database user \'#1\' does not have superuser privileges.', $dbconnect_form{dbuser}));
+}
+
 sub login {
   my ($self, $form) = @_;
 
@@ -149,8 +185,22 @@ sub login {
 
   $form->{dbupdate} = "db" . $::auth->client->{dbname};
 
-  if ($form->{"show_dbupdate_warning"}) {
-    print $form->parse_html_template("dbupgrade/warning", { unapplied_scripts => \@unapplied_scripts });
+  my $show_update_warning = $form->{"show_dbupdate_warning"};
+  my %superuser           = (need_privileges => (any { $_->{superuser_privileges} } @unapplied_scripts));
+
+  if ($superuser{need_privileges}) {
+    %superuser = (
+      %superuser,
+      $self->_handle_superuser_privileges($form),
+    );
+    $show_update_warning = 1 if !$superuser{have_privileges};
+  }
+
+  if ($show_update_warning) {
+    print $form->parse_html_template("dbupgrade/warning", {
+      unapplied_scripts => \@unapplied_scripts,
+      superuser         => \%superuser,
+    });
     $::dispatcher->end_request;
   }
 
@@ -442,6 +492,21 @@ sub dbupdate2 {
   $self->create_schema_info_table($form, $dbh);
 
   my @upgradescripts = $dbupdater->unapplied_upgrade_scripts($dbh);
+  my $need_superuser = (any { $_->{superuser_privileges} } @upgradescripts);
+  my $superuser_dbh;
+
+  if ($need_superuser) {
+    my %dbconnect_form = (
+      %{ $form },
+      dbuser   => $::auth->get_session_value("database_superuser_username"),
+      dbpasswd => $::auth->get_session_value("database_superuser_password"),
+    );
+
+    if ($dbconnect_form{dbuser} ne $form->{dbuser}) {
+      dbconnect_vars(\%dbconnect_form, $db);
+      $superuser_dbh = SL::DBConnect->connect($dbconnect_form{dbconnect}, $dbconnect_form{dbuser}, $dbconnect_form{dbpasswd}, SL::DBConnect->get_options) or $form->dberror;
+    }
+  }
 
   $::lxdebug->log_time("DB upgrades commencing");
 
@@ -449,15 +514,18 @@ sub dbupdate2 {
     # Apply upgrade. Control will only return to us if the upgrade has
     # been applied correctly and if the update has not requested user
     # interaction.
-    $main::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}");
+    my $script_dbh = $control->{superuser_privileges} ? ($superuser_dbh // $dbh) : $dbh;
+
+    $::lxdebug->message(LXDebug->DEBUG2(), "Applying Update $control->{file}" . ($control->{superuser_privileges} ? " with superuser privileges" : ""));
     print $form->parse_html_template("dbupgrade/upgrade_message2", $control) unless $silent;
 
-    $dbupdater->process_file($dbh, "sql/Pg-upgrade2/$control->{file}", $control);
+    $dbupdater->process_file($script_dbh, "sql/Pg-upgrade2/$control->{file}", $control);
   }
 
   $::lxdebug->log_time("DB upgrades finished");
 
   $dbh->disconnect;
+  $superuser_dbh->disconnect if $superuser_dbh;
 }
 
 sub data {
diff --git a/locale/de/all b/locale/de/all
index ae2e56b9e..cb5305029 100755
--- a/locale/de/all
+++ b/locale/de/all
@@ -852,6 +852,7 @@ $self->{texts} = {
   'Database login (#1)'         => 'Datenbankanmeldung (#1)',
   'Database name'               => 'Datenbankname',
   'Database settings'           => 'Datenbankeinstellungen',
+  'Database superuser privileges are required for the update.' => 'Datenbank-Super-Benutzer-Rechte werden für das Update benötigt.',
   'Database template'           => 'Datenbankvorlage',
   'Database update error:'      => 'Fehler beim Datenbankupgrade:',
   'Database user and password'  => 'Datenbankbenutzer und -passwort',
@@ -1222,6 +1223,7 @@ $self->{texts} = {
   'Error when saving: #1'       => 'Fehler beim Speichern: #1',
   'Error with default taxzone'  => 'Ungültige Standardsteuerzone',
   'Error!'                      => 'Fehler!',
+  'Error: #1'                   => 'Fehler: #1',
   'Error: A negative target quantity is not allowed.' => 'Fehler: Eine negative Zielmenge ist nicht erlaubt.',
   'Error: A quantity and a target quantity could not be given both.' => 'Fehler: Menge und Zielmenge können nicht beide angegeben werden.',
   'Error: A quantity or a target quantity must be given.' => 'Fehler: Menge oder Zielmenge muss angegeben werden.',
@@ -2237,6 +2239,7 @@ $self->{texts} = {
   'Please install the below listed modules or ask your system administrator to.' => 'Bitte installieren Sie die unten aufgeführten Module, oder bitten Sie Ihren Administrator darum.',
   'Please log in to the administration panel.' => 'Bitte melden Sie sich im Administrationsbereich an.',
   'Please modify filename'      => 'Bitte Dateinamen editieren',
+  'Please provide corresponding credentials.' => 'Bitte geben Sie entsprechende Logindaten an.',
   'Please re-run the analysis for broken general ledger entries by clicking this button:' => 'Bitte wiederholen Sie die Analyse der Hauptbucheinträge, indem Sie auf diesen Button klicken:',
   'Please read the file'        => 'Bitte lesen Sie die Datei',
   'Please select a customer from the list below.' => 'Bitte einen Endkunden aus der Liste auswählen',
@@ -3108,6 +3111,7 @@ $self->{texts} = {
   'The connection was established successfully.' => 'Die Verbindung zur Datenbank wurde erfolgreich hergestellt.',
   'The contact person attribute "birthday" is converted from a free-form text field into a date field.' => 'Das Kontaktpersonenfeld "Geburtstag" wird von einem freien Textfeld auf ein Datumsfeld umgestellt.',
   'The creation of the authentication database failed:' => 'Das Anlegen der Authentifizierungsdatenbank schlug fehl:',
+  'The credentials (username & password) for connecting database are wrong.' => 'Die Daten (Benutzername & Passwort) für das Login zur Datenbank sind falsch.',
   'The custom variable has been created.' => 'Die benutzerdefinierte Variable wurde erfasst.',
   'The custom variable has been deleted.' => 'Die benutzerdefinierte Variable wurde gelöscht.',
   'The custom variable has been saved.' => 'Die benutzerdefinierte Variable wurde gespeichert.',
@@ -3118,6 +3122,7 @@ $self->{texts} = {
   'The database name is missing.' => 'Der Datenbankname fehlt.',
   'The database port is missing.' => 'Der Datenbankport fehlt.',
   'The database update/creation did not succeed. The file #1 contained the following error:' => 'Die Datenbankaktualisierung/erstellung schlug fehl. Die Datei #1 enthielt den folgenden Fehler:',
+  'The database user \'#1\' does not have superuser privileges.' => 'Der Datenbankbenutzer »#1« hat keine Super-Benutzer-Rechte.',
   'The database user is missing.' => 'Der Datenbankbenutzer fehlt.',
   'The dataset #1 has been created.' => 'Die Datenbank #1 wurde angelegt.',
   'The dataset #1 has been deleted.' => 'Die Datenbank #1 wurde gelöscht.',
diff --git a/templates/webpages/dbupgrade/warning.html b/templates/webpages/dbupgrade/warning.html
index 297ab06aa..cd8a44a58 100644
--- a/templates/webpages/dbupgrade/warning.html
+++ b/templates/webpages/dbupgrade/warning.html
@@ -1,6 +1,6 @@
 [%- USE T8 %]
 [%- USE HTML %]
-[%- USE LxERP %]
+[%- USE LxERP %][%- USE L -%]
 <form name="Form" method="post" action="controller.pl">
 
  <input type="hidden" name="action" value="LoginScreen/login">
@@ -8,6 +8,30 @@
  <p class="message_hint">
   [% LxERP.t8('kivitendo is about to update the database [ #1 ].', dbname) | html %]
  </p>
+
+ [% IF superuser.need_privileges && !superuser.have_privileges %]
+  <p>
+   [% LxERP.t8("Database superuser privileges are required for the update.") %]
+   [% LxERP.t8("Please provide corresponding credentials.") %]
+  </p>
+
+  [% IF superuser.error %]
+   <p>[% LxERP.t8("Error: #1", superuser.error) %]</p>
+  [% END %]
+
+  <table border="0">
+   <tr>
+    <td>[% LxERP.t8("User name") %]:</td>
+    <td>[% L.input_tag("database_superuser_username", superuser.username) %]</td>
+   </tr>
+
+   <tr>
+    <td>[% LxERP.t8("Password") %]:</td>
+    <td>[% L.input_tag("database_superuser_password", superuser.password, type="password") %]</td>
+   </tr>
+  </table>
+ [% END %]
+
  <p>
   [% 'You should create a backup of the database before proceeding because the backup might not be reversible.' | $T8 %]
  </p>
