»System« → »Vorlagen« → »Stilvorlage« entfernt
[kivitendo-erp.git] / SL / Auth.pm
index bb7ae00..116d5de 100644 (file)
@@ -7,6 +7,7 @@ use IO::File;
 use Time::HiRes qw(gettimeofday);
 use List::MoreUtils qw(uniq);
 use YAML;
 use Time::HiRes qw(gettimeofday);
 use List::MoreUtils qw(uniq);
 use YAML;
+use Regexp::IPv6 qw($IPv6_re);
 
 use SL::Auth::ColumnInformation;
 use SL::Auth::Constants qw(:all);
 
 use SL::Auth::ColumnInformation;
 use SL::Auth::Constants qw(:all);
@@ -19,7 +20,7 @@ use SL::SessionFile;
 use SL::User;
 use SL::DBConnect;
 use SL::DBUpgrade2;
 use SL::User;
 use SL::DBConnect;
 use SL::DBUpgrade2;
-use SL::DBUtils;
+use SL::DBUtils qw(do_query do_statement prepare_execute_query prepare_query selectall_array_query selectrow_query);
 
 use strict;
 
 
 use strict;
 
@@ -36,20 +37,41 @@ sub new {
   my $self            = bless {}, $type;
 
   $self->_read_auth_config(%params);
   my $self            = bless {}, $type;
 
   $self->_read_auth_config(%params);
-  $self->reset;
+  $self->init;
 
   return $self;
 }
 
 
   return $self;
 }
 
-sub reset {
+sub init {
   my ($self, %params) = @_;
 
   my ($self, %params) = @_;
 
-  delete $self->{dbh};
   $self->{SESSION}            = { };
   $self->{FULL_RIGHTS}        = { };
   $self->{RIGHTS}             = { };
   $self->{unique_counter}     = 0;
   $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
   $self->{SESSION}            = { };
   $self->{FULL_RIGHTS}        = { };
   $self->{RIGHTS}             = { };
   $self->{unique_counter}     = 0;
   $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
+}
+
+sub reset {
+  my ($self, %params) = @_;
+
+  $self->{SESSION}        = { };
+  $self->{FULL_RIGHTS}    = { };
+  $self->{RIGHTS}         = { };
+  $self->{unique_counter} = 0;
+
+  if ($self->is_db_connected) {
+    # reset is called during request shutdown already. In case of a
+    # completely new auth DB this would fail and generate an error
+    # message even if the user is currently trying to create said auth
+    # DB. Therefore only fetch the column information if a connection
+    # has been established.
+    $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
+    $self->{column_information}->_fetch;
+  } else {
+    delete $self->{column_information};
+  }
+
   $self->{authenticator}->reset;
 
   $self->client(undef);
   $self->{authenticator}->reset;
 
   $self->client(undef);
@@ -89,7 +111,7 @@ sub mini_error {
   } else {
     print STDERR "Error: @msg\n";
   }
   } else {
     print STDERR "Error: @msg\n";
   }
-  ::end_of_request();
+  $::dispatcher->end_request;
 }
 
 sub _read_auth_config {
 }
 
 sub _read_auth_config {
@@ -168,8 +190,8 @@ sub authenticate_root {
     return ERR_PASSWORD;
   }
 
     return ERR_PASSWORD;
   }
 
-  $password             = SL::Auth::Password->hash(login => 'root', password => $password);
   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
+  $password             = SL::Auth::Password->hash(login => 'root', password => $password, stored_password => $admin_password);
 
   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
 
   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
@@ -236,6 +258,7 @@ sub dbconnect {
   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
 
   if (!$may_fail && !$self->{dbh}) {
   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
 
   if (!$may_fail && !$self->{dbh}) {
+    delete $self->{dbh};
     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
   }
 
     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
   }
 
@@ -251,6 +274,11 @@ sub dbdisconnect {
   }
 }
 
   }
 }
 
+sub is_db_connected {
+  my ($self) = @_;
+  return !!$self->{dbh};
+}
+
 sub check_tables {
   my ($self, $dbh)    = @_;
 
 sub check_tables {
   my ($self, $dbh)    = @_;
 
@@ -516,7 +544,7 @@ sub restore_session {
 
   $form   = $main::form;
 
 
   $form   = $main::form;
 
-  # Don't fail if the auth DB doesn't yet.
+  # Don't fail if the auth DB doesn't exist yet.
   if (!( $dbh = $self->dbconnect(1) )) {
     return $self->session_restore_result(SESSION_NONE());
   }
   if (!( $dbh = $self->dbconnect(1) )) {
     return $self->session_restore_result(SESSION_NONE());
   }
@@ -542,7 +570,7 @@ sub restore_session {
   my $api_token_cookie = $self->get_api_token_cookie;
   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
   my $api_token_cookie = $self->get_api_token_cookie;
   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
-  $cookie_is_bad     ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR}                       if !$api_token_cookie;
+  $cookie_is_bad     ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR}                       if !$api_token_cookie && $ENV{REMOTE_ADDR} !~ /^$IPv6_re$/;
   if ($cookie_is_bad) {
     $self->destroy_session();
     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
   if ($cookie_is_bad) {
     $self->destroy_session();
     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
@@ -592,47 +620,36 @@ SQL
 sub _load_with_auto_restore_column {
   my ($self, $dbh, $session_id) = @_;
 
 sub _load_with_auto_restore_column {
   my ($self, $dbh, $session_id) = @_;
 
-  my $auto_restore_keys = join ', ', map { "'${_}'" } qw(login password rpw);
+  my %auto_restore_keys = map { $_ => 1 } qw(login password rpw client_id), SESSION_KEY_ROOT_AUTH, SESSION_KEY_USER_AUTH;
 
   my $query = <<SQL;
     SELECT sess_key, sess_value, auto_restore
     FROM auth.session_content
     WHERE (session_id = ?)
 
   my $query = <<SQL;
     SELECT sess_key, sess_value, auto_restore
     FROM auth.session_content
     WHERE (session_id = ?)
-      AND (   auto_restore
-           OR sess_key IN (${auto_restore_keys}))
 SQL
   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
   while (my $ref = $sth->fetchrow_hashref) {
 SQL
   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
   while (my $ref = $sth->fetchrow_hashref) {
-    my $value = SL::Auth::SessionValue->new(auth         => $self,
-                                            key          => $ref->{sess_key},
-                                            value        => $ref->{sess_value},
-                                            auto_restore => $ref->{auto_restore},
-                                            raw          => 1);
-    $self->{SESSION}->{ $ref->{sess_key} } = $value;
-
-    next if defined $::form->{$ref->{sess_key}};
-
-    my $data                    = $value->get;
-    $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
+    if ($ref->{auto_restore} || $auto_restore_keys{$ref->{sess_key}}) {
+      my $value = SL::Auth::SessionValue->new(auth         => $self,
+                                              key          => $ref->{sess_key},
+                                              value        => $ref->{sess_value},
+                                              auto_restore => $ref->{auto_restore},
+                                              raw          => 1);
+      $self->{SESSION}->{ $ref->{sess_key} } = $value;
+
+      next if defined $::form->{$ref->{sess_key}};
+
+      my $data                    = $value->get;
+      $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
+    } else {
+      my $value = SL::Auth::SessionValue->new(auth => $self,
+                                              key  => $ref->{sess_key});
+      $self->{SESSION}->{ $ref->{sess_key} } = $value;
+    }
   }
 
   $sth->finish;
   }
 
   $sth->finish;
-
-  $query = <<SQL;
-    SELECT sess_key
-    FROM auth.session_content
-    WHERE (session_id = ?)
-      AND NOT COALESCE(auto_restore, FALSE)
-      AND (sess_key NOT IN (${auto_restore_keys}))
-SQL
-  $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
-
-  while (my $ref = $sth->fetchrow_hashref) {
-    my $value = SL::Auth::SessionValue->new(auth => $self,
-                                            key  => $ref->{sess_key});
-    $self->{SESSION}->{ $ref->{sess_key} } = $value;
-  }
 }
 
 sub destroy_session {
 }
 
 sub destroy_session {
@@ -897,104 +914,52 @@ sub is_api_token_cookie_valid {
   return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
 }
 
   return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
 }
 
-sub session_tables_present {
-  my $self = shift;
+sub _tables_present {
+  my ($self, @tables) = @_;
+  my $cache_key = join '_', @tables;
 
   # Only re-check for the presence of auth tables if either the check
   # hasn't been done before of if they weren't present.
 
   # Only re-check for the presence of auth tables if either the check
   # hasn't been done before of if they weren't present.
-  if ($self->{session_tables_present}) {
-    return $self->{session_tables_present};
-  }
+  return $self->{"$cache_key\_tables_present"} ||= do {
+    my $dbh  = $self->dbconnect(1);
 
 
-  my $dbh  = $self->dbconnect(1);
+    if (!$dbh) {
+      return 0;
+    }
 
 
-  if (!$dbh) {
-    return 0;
-  }
+    my $query =
+      qq|SELECT COUNT(*)
+         FROM pg_tables
+         WHERE (schemaname = 'auth')
+           AND (tablename IN (@{[ join ', ', ('?') x @tables ]}))|;
 
 
-  my $query =
-    qq|SELECT COUNT(*)
-       FROM pg_tables
-       WHERE (schemaname = 'auth')
-         AND (tablename IN ('session', 'session_content'))|;
+    my ($count) = selectrow_query($main::form, $dbh, $query, @tables);
 
 
-  my ($count) = selectrow_query($main::form, $dbh, $query);
+    scalar @tables == $count;
+  }
+}
 
 
-  $self->{session_tables_present} = 2 == $count;
+sub session_tables_present {
+  $_[0]->_tables_present('session', 'session_content');
+}
 
 
-  return $self->{session_tables_present};
+sub master_rights_present {
+  $_[0]->_tables_present('master_rights');
 }
 
 # --------------------------------------
 
 sub all_rights_full {
 }
 
 # --------------------------------------
 
 sub all_rights_full {
-  my $locale = $main::locale;
-
-  my @all_rights = (
-    ["--crm",                          $locale->text("CRM optional software")],
-    ["crm_search",                     $locale->text("CRM search")],
-    ["crm_new",                        $locale->text("CRM create customers, vendors and contacts")],
-    ["crm_service",                    $locale->text("CRM services")],
-    ["crm_admin",                      $locale->text("CRM admin")],
-    ["crm_adminuser",                  $locale->text("CRM user")],
-    ["crm_adminstatus",                $locale->text("CRM status")],
-    ["crm_email",                      $locale->text("CRM send email")],
-    ["crm_termin",                     $locale->text("CRM termin")],
-    ["crm_opportunity",                $locale->text("CRM opportunity")],
-    ["crm_knowhow",                    $locale->text("CRM know how")],
-    ["crm_follow",                     $locale->text("CRM follow up")],
-    ["crm_notices",                    $locale->text("CRM notices")],
-    ["crm_other",                      $locale->text("CRM other")],
-    ["--master_data",                  $locale->text("Master Data")],
-    ["customer_vendor_edit",           $locale->text("Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)")],
-    ["customer_vendor_all_edit",       $locale->text("Create customers and vendors. Edit all vendors. Edit all customers")],
-    ["part_service_assembly_edit",     $locale->text("Create and edit parts, services, assemblies")],
-    ["part_service_assembly_details",  $locale->text("Show details and reports of parts, services, assemblies")],
-    ["project_edit",                   $locale->text("Create and edit projects")],
-    ["--ar",                           $locale->text("AR")],
-    ["requirement_spec_edit",          $locale->text("Create and edit requirement specs")],
-    ["sales_quotation_edit",           $locale->text("Create and edit sales quotations")],
-    ["sales_order_edit",               $locale->text("Create and edit sales orders")],
-    ["sales_delivery_order_edit",      $locale->text("Create and edit sales delivery orders")],
-    ["invoice_edit",                   $locale->text("Create and edit invoices and credit notes")],
-    ["dunning_edit",                   $locale->text("Create and edit dunnings")],
-    ["sales_all_edit",                 $locale->text("View/edit all employees sales documents")],
-    ["edit_prices",                    $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
-    ["show_ar_transactions",           $locale->text("Show AR transactions as part of AR invoice report")],
-    ["delivery_plan",                  $locale->text("Show delivery plan")],
-    ["delivery_value_report",          $locale->text("Show delivery value report")],
-    ["--ap",                           $locale->text("AP")],
-    ["request_quotation_edit",         $locale->text("Create and edit RFQs")],
-    ["purchase_order_edit",            $locale->text("Create and edit purchase orders")],
-    ["purchase_delivery_order_edit",   $locale->text("Create and edit purchase delivery orders")],
-    ["vendor_invoice_edit",            $locale->text("Create and edit vendor invoices")],
-    ["show_ap_transactions",           $locale->text("Show AP transactions as part of AP invoice report")],
-    ["--warehouse_management",         $locale->text("Warehouse management")],
-    ["warehouse_contents",             $locale->text("View warehouse content")],
-    ["warehouse_management",           $locale->text("Warehouse management")],
-    ["--general_ledger_cash",          $locale->text("General ledger and cash")],
-    ["general_ledger",                 $locale->text("Transactions, AR transactions, AP transactions")],
-    ["datev_export",                   $locale->text("DATEV Export")],
-    ["cash",                           $locale->text("Receipt, payment, reconciliation")],
-    ["--reports",                      $locale->text('Reports')],
-    ["report",                         $locale->text('All reports')],
-    ["advance_turnover_tax_return",    $locale->text('Advance turnover tax return')],
-    ["--batch_printing",               $locale->text("Batch Printing")],
-    ["batch_printing",                 $locale->text("Batch Printing")],
-    ["--configuration",                $locale->text("Configuration")],
-    ["config",                         $locale->text("Change kivitendo installation settings (most entries in the 'System' menu)")],
-    ["admin",                          $locale->text("Client administration: configuration, editing templates, task server control, background jobs (remaining entries in the 'System' menu)")],
-    ["--others",                       $locale->text("Others")],
-    ["email_bcc",                      $locale->text("May set the BCC field when sending emails")],
-    ["productivity",                   $locale->text("Productivity")],
-    ["display_admin_link",             $locale->text("Show administration link")],
-    );
-
-  return @all_rights;
+  my ($self) = @_;
+
+  @{ $self->{master_rights} ||= do {
+      $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY position");
+    }
+  }
 }
 
 sub all_rights {
 }
 
 sub all_rights {
-  return grep !/^--/, map { $_->[0] } all_rights_full();
+  return map { $_->[0] } grep { !$_->[2] } $_[0]->all_rights_full;
 }
 
 sub read_groups {
 }
 
 sub read_groups {
@@ -1041,7 +1006,7 @@ sub read_groups {
       $group->{rights}->{$row->{right}} |= $row->{granted};
     }
 
       $group->{rights}->{$row->{right}} |= $row->{granted};
     }
 
-    map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } all_rights();
+    map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } $self->all_rights;
   }
   $sth->finish();
 
   }
   $sth->finish();
 
@@ -1172,7 +1137,7 @@ sub _parse_rights_string {
       push @{$cur_ary}, $token;
 
     } else {
       push @{$cur_ary}, $token;
 
     } else {
-      push @{$cur_ary}, $self->{RIGHTS}->{$login}->{$token} * 1;
+      push @{$cur_ary}, ($self->{RIGHTS}->{$login}->{$token} // 0) * 1;
     }
   }
 
     }
   }
 
@@ -1223,7 +1188,9 @@ sub load_rights_for_user {
   my $dbh   = $self->dbconnect;
   my ($query, $sth, $row, $rights);
 
   my $dbh   = $self->dbconnect;
   my ($query, $sth, $row, $rights);
 
-  $rights = { map { $_ => 0 } all_rights() };
+  $rights = { map { $_ => 0 } $self->all_rights };
+
+  return $rights if !$self->client || !$login;
 
   $query =
     qq|SELECT gr."right", gr.granted
 
   $query =
     qq|SELECT gr."right", gr.granted
@@ -1259,7 +1226,7 @@ __END__
 
 SL::Auth - Authentication and session handling
 
 
 SL::Auth - Authentication and session handling
 
-=head1 FUNCTIONS
+=head1 METHODS
 
 =over 4
 
 
 =over 4
 
@@ -1309,7 +1276,7 @@ Stores the session values in the database. This is the only function
 that actually stores stuff in the database. Neither the various
 setters nor the deleter access the database.
 
 that actually stores stuff in the database. Neither the various
 setters nor the deleter access the database.
 
-=item <save_form_in_session %params>
+=item C<save_form_in_session %params>
 
 Stores the content of C<$params{form}> (default: C<$::form>) in the
 session using L</create_unique_sesion_value>.
 
 Stores the content of C<$params{form}> (default: C<$::form>) in the
 session using L</create_unique_sesion_value>.
@@ -1323,7 +1290,7 @@ can be given as an array ref in C<$params{skip_keys}>.
 
 Returns the unique key under which the form is stored.
 
 
 Returns the unique key under which the form is stored.
 
-=item <restore_form_from_session $key, %params>
+=item C<restore_form_from_session $key, %params>
 
 Restores the form from the session into C<$params{form}> (default:
 C<$::form>).
 
 Restores the form from the session into C<$params{form}> (default:
 C<$::form>).
@@ -1334,6 +1301,14 @@ is on by default.
 
 Returns C<$self>.
 
 
 Returns C<$self>.
 
+=item C<reset>
+
+C<reset> deletes every state information from previous requests, but does not
+close the database connection.
+
+Creating a new database handle on each request can take up to 30% of the
+pre-request startup time, so we want to avoid that for fast ajax calls.
+
 =back
 
 =head1 BUGS
 =back
 
 =head1 BUGS