5 use Digest::MD5 qw(md5_hex);
 
   7 use Time::HiRes qw(gettimeofday);
 
   8 use List::MoreUtils qw(uniq);
 
  11 use SL::Auth::ColumnInformation;
 
  12 use SL::Auth::Constants qw(:all);
 
  15 use SL::Auth::Password;
 
  16 use SL::Auth::SessionValue;
 
  26 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
 
  27 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
 
  29 use Rose::Object::MakeMethods::Generic (
 
  30   scalar => [ qw(client) ],
 
  35   $main::lxdebug->enter_sub();
 
  37   my ($type, %params) = @_;
 
  38   my $self            = bless {}, $type;
 
  40   $self->_read_auth_config(%params);
 
  43   $main::lxdebug->leave_sub();
 
  49   my ($self, %params) = @_;
 
  52   $self->{SESSION}            = { };
 
  53   $self->{FULL_RIGHTS}        = { };
 
  54   $self->{RIGHTS}             = { };
 
  55   $self->{unique_counter}     = 0;
 
  56   $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
 
  57   $self->{authenticator}->reset;
 
  63   my ($self, $id_or_name) = @_;
 
  67   return undef unless $id_or_name;
 
  69   my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
 
  70   my $dbh    = $self->dbconnect;
 
  72   return undef unless $dbh;
 
  74   $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
 
  82   $self->{dbh}->disconnect() if ($self->{dbh});
 
  85 # form isn't loaded yet, so auth needs it's own error.
 
  87   $::lxdebug->show_backtrace();
 
  89   my ($self, @msg) = @_;
 
  90   if ($ENV{HTTP_USER_AGENT}) {
 
  91     print Form->create_http_response(content_type => 'text/html');
 
  92     print "<pre>", join ('<br>', @msg), "</pre>";
 
  94     print STDERR "Error: @msg\n";
 
  99 sub _read_auth_config {
 
 100   $main::lxdebug->enter_sub();
 
 102   my ($self, %params) = @_;
 
 104   map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
 
 106   # Prevent password leakage to log files when dumping Auth instances.
 
 107   $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
 
 109   if ($params{unit_tests_database}) {
 
 110     $self->{DB_config}   = $::lx_office_conf{'testing/database'};
 
 111     $self->{module}      = 'DB';
 
 114     $self->{DB_config}   = $::lx_office_conf{'authentication/database'};
 
 115     $self->{LDAP_config} = $::lx_office_conf{'authentication/ldap'};
 
 118   if ($self->{module} eq 'DB') {
 
 119     $self->{authenticator} = SL::Auth::DB->new($self);
 
 121   } elsif ($self->{module} eq 'LDAP') {
 
 122     $self->{authenticator} = SL::Auth::LDAP->new($self);
 
 125   if (!$self->{authenticator}) {
 
 126     my $locale = Locale->new('en');
 
 127     $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
 
 130   my $cfg = $self->{DB_config};
 
 133     my $locale = Locale->new('en');
 
 134     $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
 
 137   if (!$cfg->{host} || !$cfg->{db} || !$cfg->{user}) {
 
 138     my $locale = Locale->new('en');
 
 139     $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
 
 142   $self->{authenticator}->verify_config();
 
 144   $self->{session_timeout} *= 1;
 
 145   $self->{session_timeout}  = 8 * 60 if (!$self->{session_timeout});
 
 147   $main::lxdebug->leave_sub();
 
 150 sub has_access_to_client {
 
 151   my ($self, $login) = @_;
 
 153   return 0 if !$self->client || !$self->client->{id};
 
 157     FROM auth.clients_users cu
 
 158     LEFT JOIN auth."user" u ON (cu.user_id = u.id)
 
 160       AND (cu.client_id = ?)
 
 163   my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
 
 167 sub authenticate_root {
 
 168   $main::lxdebug->enter_sub();
 
 170   my ($self, $password) = @_;
 
 172   my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
 
 173   if (defined $session_root_auth && $session_root_auth == OK) {
 
 174     $::lxdebug->leave_sub;
 
 178   if (!defined $password) {
 
 179     $::lxdebug->leave_sub;
 
 183   $password             = SL::Auth::Password->hash(login => 'root', password => $password);
 
 184   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
 
 186   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
 
 187   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
 
 189   $::lxdebug->leave_sub;
 
 194   $main::lxdebug->enter_sub();
 
 196   my ($self, $login, $password) = @_;
 
 198   if (!$self->client || !$self->has_access_to_client($login)) {
 
 199     $::lxdebug->leave_sub;
 
 203   my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
 
 204   if (defined $session_auth && $session_auth == OK) {
 
 205     $::lxdebug->leave_sub;
 
 209   if (!defined $password) {
 
 210     $::lxdebug->leave_sub;
 
 214   my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
 
 215   $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
 
 217   $::lxdebug->leave_sub;
 
 221 sub punish_wrong_login {
 
 222   my $failed_login_penalty = ($::lx_office_conf{authentication} || {})->{failed_login_penalty};
 
 223   sleep $failed_login_penalty if $failed_login_penalty;
 
 226 sub get_stored_password {
 
 227   my ($self, $login) = @_;
 
 229   my $dbh            = $self->dbconnect;
 
 231   return undef unless $dbh;
 
 233   my $query             = qq|SELECT password FROM auth."user" WHERE login = ?|;
 
 234   my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
 
 236   return $stored_password;
 
 240   $main::lxdebug->enter_sub(2);
 
 243   my $may_fail = shift;
 
 246     $main::lxdebug->leave_sub(2);
 
 250   my $cfg = $self->{DB_config};
 
 251   my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
 
 254     $dsn .= ';port=' . $cfg->{port};
 
 257   $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
 
 259   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
 
 261   if (!$may_fail && !$self->{dbh}) {
 
 262     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
 
 265   $main::lxdebug->leave_sub(2);
 
 271   $main::lxdebug->enter_sub();
 
 276     $self->{dbh}->disconnect();
 
 280   $main::lxdebug->leave_sub();
 
 284   $main::lxdebug->enter_sub();
 
 286   my ($self, $dbh)    = @_;
 
 288   $dbh   ||= $self->dbconnect();
 
 289   my $query   = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
 
 291   my ($count) = $dbh->selectrow_array($query);
 
 293   $main::lxdebug->leave_sub();
 
 299   $main::lxdebug->enter_sub();
 
 303   my $dbh  = $self->dbconnect(1);
 
 305   $main::lxdebug->leave_sub();
 
 310 sub create_database {
 
 311   $main::lxdebug->enter_sub();
 
 316   my $cfg    = $self->{DB_config};
 
 318   if (!$params{superuser}) {
 
 319     $params{superuser}          = $cfg->{user};
 
 320     $params{superuser_password} = $cfg->{password};
 
 323   $params{template} ||= 'template0';
 
 324   $params{template}   =~ s|[^a-zA-Z0-9_\-]||g;
 
 326   my $dsn = 'dbi:Pg:dbname=template1;host=' . $cfg->{host};
 
 329     $dsn .= ';port=' . $cfg->{port};
 
 332   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database DSN: $dsn");
 
 334   my $dbh = SL::DBConnect->connect($dsn, $params{superuser}, $params{superuser_password}, { pg_enable_utf8 => 1 });
 
 337     $main::form->error($main::locale->text('The connection to the template database failed:') . "\n" . $DBI::errstr);
 
 340   my $query = qq|CREATE DATABASE "$cfg->{db}" OWNER "$cfg->{user}" TEMPLATE "$params{template}" ENCODING 'UNICODE'|;
 
 342   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database query: $query");
 
 347     my $error = $dbh->errstr();
 
 349     $query                 = qq|SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'|;
 
 350     my ($cluster_encoding) = $dbh->selectrow_array($query);
 
 352     if ($cluster_encoding && ($cluster_encoding !~ m/^(?:UTF-?8|UNICODE)$/i)) {
 
 353       $error = $::locale->text('Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.');
 
 358     $main::form->error($main::locale->text('The creation of the authentication database failed:') . "\n" . $error);
 
 363   $main::lxdebug->leave_sub();
 
 367   $main::lxdebug->enter_sub();
 
 370   my $dbh  = $self->dbconnect();
 
 373   SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
 
 375   $main::lxdebug->leave_sub();
 
 379   $main::lxdebug->enter_sub();
 
 385   my $form   = $main::form;
 
 387   my $dbh    = $self->dbconnect();
 
 389   my ($sth, $query, $user_id);
 
 393   $query     = qq|SELECT id FROM auth."user" WHERE login = ?|;
 
 394   ($user_id) = selectrow_query($form, $dbh, $query, $login);
 
 397     $query     = qq|SELECT nextval('auth.user_id_seq')|;
 
 398     ($user_id) = selectrow_query($form, $dbh, $query);
 
 400     $query     = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
 
 401     do_query($form, $dbh, $query, $user_id, $login);
 
 404   $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
 
 405   do_query($form, $dbh, $query, $user_id);
 
 407   $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
 
 408   $sth   = prepare_query($form, $dbh, $query);
 
 410   while (my ($cfg_key, $cfg_value) = each %params) {
 
 411     next if ($cfg_key eq 'password');
 
 413     do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
 
 418   $main::lxdebug->leave_sub();
 
 421 sub can_change_password {
 
 424   return $self->{authenticator}->can_change_password();
 
 427 sub change_password {
 
 428   $main::lxdebug->enter_sub();
 
 430   my ($self, $login, $new_password) = @_;
 
 432   my $result = $self->{authenticator}->change_password($login, $new_password);
 
 434   $main::lxdebug->leave_sub();
 
 440   $main::lxdebug->enter_sub();
 
 444   my $dbh   = $self->dbconnect();
 
 445   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value, s.mtime AS last_action
 
 447                  FROM auth."user" AS  u
 
 449                  LEFT JOIN auth.user_config AS cfg
 
 450                    ON (cfg.user_id = u.id)
 
 452                  LEFT JOIN auth.session_content AS sc_login
 
 453                    ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
 
 455                  LEFT JOIN auth.session AS s
 
 456                    ON (s.id = sc_login.session_id)
 
 458   my $sth   = prepare_execute_query($main::form, $dbh, $query);
 
 462   while (my $ref = $sth->fetchrow_hashref()) {
 
 464     $users{$ref->{login}}                    ||= {
 
 465                                                 'login' => $ref->{login},
 
 467                                                 'last_action' => $ref->{last_action},
 
 469     $users{$ref->{login}}->{$ref->{cfg_key}}   = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
 
 474   $main::lxdebug->leave_sub();
 
 480   $main::lxdebug->enter_sub();
 
 482   my ($self, %params) = @_;
 
 484   my $dbh   = $self->dbconnect();
 
 486   my (@where, @values);
 
 487   if ($params{login}) {
 
 488     push @where,  'u.login = ?';
 
 489     push @values, $params{login};
 
 492     push @where,  'u.id = ?';
 
 493     push @values, $params{id};
 
 495   my $where = join ' AND ', '1 = 1', @where;
 
 496   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
 
 497                  FROM auth.user_config cfg
 
 498                  LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
 
 500   my $sth   = prepare_execute_query($main::form, $dbh, $query, @values);
 
 504   while (my $ref = $sth->fetchrow_hashref()) {
 
 505     $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
 
 506     @user_data{qw(id login)}    = @{$ref}{qw(id login)};
 
 509   # The XUL/XML & 'CSS new' backed menus have been removed.
 
 510   my %menustyle_map = ( xml => 'new', v4 => 'v3' );
 
 511   $user_data{menustyle} = $menustyle_map{lc($user_data{menustyle} || '')} || $user_data{menustyle};
 
 513   # The 'Win2000.css' stylesheet has been removed.
 
 514   $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
 
 516   # Set default language if selected language does not exist (anymore).
 
 517   $user_data{countrycode} = $::lx_office_conf{system}->{language} unless $user_data{countrycode} && -d "locale/$user_data{countrycode}";
 
 521   $main::lxdebug->leave_sub();
 
 527   $main::lxdebug->enter_sub();
 
 532   my $dbh   = $self->dbconnect();
 
 533   my ($id)  = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
 
 535   $main::lxdebug->leave_sub();
 
 541   $::lxdebug->enter_sub;
 
 546   my $dbh   = $self->dbconnect;
 
 547   my $id    = $self->get_user_id($login);
 
 549   $dbh->rollback and return $::lxdebug->leave_sub if (!$id);
 
 553   do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
 
 554   do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
 
 555   do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
 
 557   # TODO: SL::Auth::delete_user
 
 558   # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
 
 562   $::lxdebug->leave_sub;
 
 565 # --------------------------------------
 
 569 sub restore_session {
 
 570   $main::lxdebug->enter_sub();
 
 574   $session_id        =  $::request->{cgi}->cookie($self->get_session_cookie_name());
 
 575   $session_id        =~ s|[^0-9a-f]||g if $session_id;
 
 577   $self->{SESSION}   = { };
 
 580     $main::lxdebug->leave_sub();
 
 581     return $self->session_restore_result(SESSION_NONE());
 
 584   my ($dbh, $query, $sth, $cookie, $ref, $form);
 
 588   # Don't fail if the auth DB doesn't yet.
 
 589   if (!( $dbh = $self->dbconnect(1) )) {
 
 590     $::lxdebug->leave_sub;
 
 591     return $self->session_restore_result(SESSION_NONE());
 
 594   # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
 
 595   # admin is creating the session tables at the moment.
 
 596   $query  = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
 
 598   if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
 
 599     $sth->finish if $sth;
 
 600     $::lxdebug->leave_sub;
 
 601     return $self->session_restore_result(SESSION_NONE());
 
 604   $cookie = $sth->fetchrow_hashref;
 
 607   # The session ID provided is valid in the following cases:
 
 608   #  1. session ID exists in the database
 
 609   #  2. hasn't expired yet
 
 610   #  3. if cookie for the API token is given: the cookie's value equal database column 'auth.session.api_token' for the session ID
 
 611   #  4. if cookie for the API token is NOT given then: the requestee's IP address must match the stored IP address
 
 612   $self->{api_token}   = $cookie->{api_token} if $cookie;
 
 613   my $api_token_cookie = $self->get_api_token_cookie;
 
 614   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
 
 615   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
 
 616   $cookie_is_bad     ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR}                       if !$api_token_cookie;
 
 617   if ($cookie_is_bad) {
 
 618     $self->destroy_session();
 
 619     $main::lxdebug->leave_sub();
 
 620     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
 
 623   if ($self->{column_information}->has('auto_restore')) {
 
 624     $self->_load_with_auto_restore_column($dbh, $session_id);
 
 626     $self->_load_without_auto_restore_column($dbh, $session_id);
 
 629   $main::lxdebug->leave_sub();
 
 631   return $self->session_restore_result(SESSION_OK());
 
 634 sub session_restore_result {
 
 637     $self->{session_restore_result} = $_[0];
 
 639   return $self->{session_restore_result};
 
 642 sub _load_without_auto_restore_column {
 
 643   my ($self, $dbh, $session_id) = @_;
 
 646     SELECT sess_key, sess_value
 
 647     FROM auth.session_content
 
 648     WHERE (session_id = ?)
 
 650   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
 652   while (my $ref = $sth->fetchrow_hashref) {
 
 653     my $value = SL::Auth::SessionValue->new(auth  => $self,
 
 654                                             key   => $ref->{sess_key},
 
 655                                             value => $ref->{sess_value},
 
 657     $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 659     next if defined $::form->{$ref->{sess_key}};
 
 661     my $data                    = $value->get;
 
 662     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
 
 666 sub _load_with_auto_restore_column {
 
 667   my ($self, $dbh, $session_id) = @_;
 
 669   my $auto_restore_keys = join ', ', map { "'${_}'" } qw(login password rpw);
 
 672     SELECT sess_key, sess_value, auto_restore
 
 673     FROM auth.session_content
 
 674     WHERE (session_id = ?)
 
 676            OR sess_key IN (${auto_restore_keys}))
 
 678   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
 680   while (my $ref = $sth->fetchrow_hashref) {
 
 681     my $value = SL::Auth::SessionValue->new(auth         => $self,
 
 682                                             key          => $ref->{sess_key},
 
 683                                             value        => $ref->{sess_value},
 
 684                                             auto_restore => $ref->{auto_restore},
 
 686     $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 688     next if defined $::form->{$ref->{sess_key}};
 
 690     my $data                    = $value->get;
 
 691     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
 
 698     FROM auth.session_content
 
 699     WHERE (session_id = ?)
 
 700       AND NOT COALESCE(auto_restore, FALSE)
 
 701       AND (sess_key NOT IN (${auto_restore_keys}))
 
 703   $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
 705   while (my $ref = $sth->fetchrow_hashref) {
 
 706     my $value = SL::Auth::SessionValue->new(auth => $self,
 
 707                                             key  => $ref->{sess_key});
 
 708     $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 712 sub destroy_session {
 
 713   $main::lxdebug->enter_sub();
 
 718     my $dbh = $self->dbconnect();
 
 722     do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
 
 723     do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
 
 727     SL::SessionFile->destroy_session($session_id);
 
 730     $self->{SESSION} = { };
 
 733   $main::lxdebug->leave_sub();
 
 736 sub active_session_ids {
 
 738   my $dbh   = $self->dbconnect;
 
 740   my $query = qq|SELECT id FROM auth.session|;
 
 742   my @ids   = selectall_array_query($::form, $dbh, $query);
 
 747 sub expire_sessions {
 
 748   $main::lxdebug->enter_sub();
 
 752   $main::lxdebug->leave_sub and return if !$self->session_tables_present;
 
 754   my $dbh   = $self->dbconnect();
 
 756   my $query = qq|SELECT id
 
 758                  WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
 
 760   my @ids   = selectall_array_query($::form, $dbh, $query);
 
 765     SL::SessionFile->destroy_session($_) for @ids;
 
 767     $query = qq|DELETE FROM auth.session_content
 
 768                 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
 
 769     do_query($main::form, $dbh, $query, @ids);
 
 771     $query = qq|DELETE FROM auth.session
 
 772                 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
 
 773     do_query($main::form, $dbh, $query, @ids);
 
 778   $main::lxdebug->leave_sub();
 
 781 sub _create_session_id {
 
 782   $main::lxdebug->enter_sub();
 
 785   map { push @data, int(rand() * 255); } (1..32);
 
 787   my $id = md5_hex(pack 'C*', @data);
 
 789   $main::lxdebug->leave_sub();
 
 794 sub create_or_refresh_session {
 
 795   $session_id ||= shift->_create_session_id;
 
 799   $::lxdebug->enter_sub;
 
 801   my $provided_dbh = shift;
 
 803   my $dbh          = $provided_dbh || $self->dbconnect(1);
 
 805   $::lxdebug->leave_sub && return unless $dbh && $session_id;
 
 807   $dbh->begin_work unless $provided_dbh;
 
 809   # If this fails then the "auth" schema might not exist yet, e.g. if
 
 810   # the admin is just trying to create the auth database.
 
 811   if (!$dbh->do(qq|LOCK auth.session_content|)) {
 
 812     $dbh->rollback unless $provided_dbh;
 
 813     $::lxdebug->leave_sub;
 
 817   my @unfetched_keys = map     { $_->{key}        }
 
 818                        grep    { ! $_->{fetched}  }
 
 819                        values %{ $self->{SESSION} };
 
 820   # $::lxdebug->dump(0, "unfetched_keys", [ sort @unfetched_keys ]);
 
 821   # $::lxdebug->dump(0, "all keys", [ sort map { $_->{key} } values %{ $self->{SESSION} } ]);
 
 822   my $query          = qq|DELETE FROM auth.session_content WHERE (session_id = ?)|;
 
 823   $query            .= qq| AND (sess_key NOT IN (| . join(', ', ('?') x scalar @unfetched_keys) . qq|))| if @unfetched_keys;
 
 825   do_query($::form, $dbh, $query, $session_id, @unfetched_keys);
 
 827   my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
 
 830     do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
 
 832     do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
 
 835   if ($self->{column_information}->has('api_token', 'session')) {
 
 836     my ($stored_api_token) = $dbh->selectrow_array(qq|SELECT api_token FROM auth.session WHERE id = ?|, undef, $session_id);
 
 837     do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
 
 840   my @values_to_save = grep    { $_->{fetched} }
 
 841                        values %{ $self->{SESSION} };
 
 842   if (@values_to_save) {
 
 843     my ($columns, $placeholders) = ('', '');
 
 844     my $auto_restore             = $self->{column_information}->has('auto_restore');
 
 847       $columns      .= ', auto_restore';
 
 848       $placeholders .= ', ?';
 
 851     $query  = qq|INSERT INTO auth.session_content (session_id, sess_key, sess_value ${columns}) VALUES (?, ?, ? ${placeholders})|;
 
 852     my $sth = prepare_query($::form, $dbh, $query);
 
 854     foreach my $value (@values_to_save) {
 
 855       my @values = ($value->{key}, $value->get_dumped);
 
 856       push @values, $value->{auto_restore} if $auto_restore;
 
 858       do_statement($::form, $sth, $query, $session_id, @values);
 
 864   $dbh->commit() unless $provided_dbh;
 
 865   $::lxdebug->leave_sub;
 
 868 sub set_session_value {
 
 869   $main::lxdebug->enter_sub();
 
 874   $self->{SESSION} ||= { };
 
 877     my $key = shift @params;
 
 879     if (ref $key eq 'HASH') {
 
 880       $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key          => $key->{key},
 
 881                                                                       value        => $key->{value},
 
 882                                                                       auto_restore => $key->{auto_restore});
 
 885       my $value = shift @params;
 
 886       $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key   => $key,
 
 891   $main::lxdebug->leave_sub();
 
 896 sub delete_session_value {
 
 897   $main::lxdebug->enter_sub();
 
 901   $self->{SESSION} ||= { };
 
 902   delete @{ $self->{SESSION} }{ @_ };
 
 904   $main::lxdebug->leave_sub();
 
 909 sub get_session_value {
 
 910   $main::lxdebug->enter_sub();
 
 913   my $data = $self->{SESSION} && $self->{SESSION}->{ $_[0] } ? $self->{SESSION}->{ $_[0] }->get : undef;
 
 915   $main::lxdebug->leave_sub();
 
 920 sub create_unique_sesion_value {
 
 921   my ($self, $value, %params) = @_;
 
 923   $self->{SESSION} ||= { };
 
 925   my @now                   = gettimeofday();
 
 926   my $key                   = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
 
 927   $self->{unique_counter} ||= 0;
 
 931     $self->{unique_counter}++;
 
 932     $hashed_key = md5_hex($key . $self->{unique_counter});
 
 933   } while (exists $self->{SESSION}->{$hashed_key});
 
 935   $self->set_session_value($hashed_key => $value);
 
 940 sub save_form_in_session {
 
 941   my ($self, %params) = @_;
 
 943   my $form        = delete($params{form}) || $::form;
 
 944   my $non_scalars = delete $params{non_scalars};
 
 947   my %skip_keys   = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
 
 949   foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
 
 950     $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
 
 953   return $self->create_unique_sesion_value($data, %params);
 
 956 sub restore_form_from_session {
 
 957   my ($self, $key, %params) = @_;
 
 959   my $data = $self->get_session_value($key);
 
 960   return $self unless $data;
 
 962   my $form    = delete($params{form}) || $::form;
 
 963   my $clobber = exists $params{clobber} ? $params{clobber} : 1;
 
 965   map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
 
 970 sub set_cookie_environment_variable {
 
 972   $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
 
 975 sub get_session_cookie_name {
 
 976   my ($self, %params) = @_;
 
 978   $params{type}     ||= 'id';
 
 979   my $name            = $self->{cookie_name} || 'lx_office_erp_session_id';
 
 980   $name              .= '_api_token' if $params{type} eq 'api_token';
 
 989 sub get_api_token_cookie {
 
 992   $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
 
 995 sub is_api_token_cookie_valid {
 
 997   my $provided_api_token = $self->get_api_token_cookie;
 
 998   return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
 
1001 sub session_tables_present {
 
1002   $main::lxdebug->enter_sub();
 
1006   # Only re-check for the presence of auth tables if either the check
 
1007   # hasn't been done before of if they weren't present.
 
1008   if ($self->{session_tables_present}) {
 
1009     $main::lxdebug->leave_sub();
 
1010     return $self->{session_tables_present};
 
1013   my $dbh  = $self->dbconnect(1);
 
1016     $main::lxdebug->leave_sub();
 
1023        WHERE (schemaname = 'auth')
 
1024          AND (tablename IN ('session', 'session_content'))|;
 
1026   my ($count) = selectrow_query($main::form, $dbh, $query);
 
1028   $self->{session_tables_present} = 2 == $count;
 
1030   $main::lxdebug->leave_sub();
 
1032   return $self->{session_tables_present};
 
1035 # --------------------------------------
 
1037 sub all_rights_full {
 
1038   my $locale = $main::locale;
 
1041     ["--crm",                          $locale->text("CRM optional software")],
 
1042     ["crm_search",                     $locale->text("CRM search")],
 
1043     ["crm_new",                        $locale->text("CRM create customers, vendors and contacts")],
 
1044     ["crm_service",                    $locale->text("CRM services")],
 
1045     ["crm_admin",                      $locale->text("CRM admin")],
 
1046     ["crm_adminuser",                  $locale->text("CRM user")],
 
1047     ["crm_adminstatus",                $locale->text("CRM status")],
 
1048     ["crm_email",                      $locale->text("CRM send email")],
 
1049     ["crm_termin",                     $locale->text("CRM termin")],
 
1050     ["crm_opportunity",                $locale->text("CRM opportunity")],
 
1051     ["crm_knowhow",                    $locale->text("CRM know how")],
 
1052     ["crm_follow",                     $locale->text("CRM follow up")],
 
1053     ["crm_notices",                    $locale->text("CRM notices")],
 
1054     ["crm_other",                      $locale->text("CRM other")],
 
1055     ["--master_data",                  $locale->text("Master Data")],
 
1056     ["customer_vendor_edit",           $locale->text("Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)")],
 
1057     ["customer_vendor_all_edit",       $locale->text("Create customers and vendors. Edit all vendors. Edit all customers")],
 
1058     ["part_service_assembly_edit",     $locale->text("Create and edit parts, services, assemblies")],
 
1059     ["part_service_assembly_details",  $locale->text("Show details and reports of parts, services, assemblies")],
 
1060     ["project_edit",                   $locale->text("Create and edit projects")],
 
1061     ["--ar",                           $locale->text("AR")],
 
1062     ["requirement_spec_edit",          $locale->text("Create and edit requirement specs")],
 
1063     ["sales_quotation_edit",           $locale->text("Create and edit sales quotations")],
 
1064     ["sales_order_edit",               $locale->text("Create and edit sales orders")],
 
1065     ["sales_delivery_order_edit",      $locale->text("Create and edit sales delivery orders")],
 
1066     ["invoice_edit",                   $locale->text("Create and edit invoices and credit notes")],
 
1067     ["dunning_edit",                   $locale->text("Create and edit dunnings")],
 
1068     ["sales_all_edit",                 $locale->text("View/edit all employees sales documents")],
 
1069     ["edit_prices",                    $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
 
1070     ["show_ar_transactions",           $locale->text("Show AR transactions as part of AR invoice report")],
 
1071     ["delivery_plan",                  $locale->text("Show delivery plan")],
 
1072     ["delivery_value_report",          $locale->text("Show delivery value report")],
 
1073     ["--ap",                           $locale->text("AP")],
 
1074     ["request_quotation_edit",         $locale->text("Create and edit RFQs")],
 
1075     ["purchase_order_edit",            $locale->text("Create and edit purchase orders")],
 
1076     ["purchase_delivery_order_edit",   $locale->text("Create and edit purchase delivery orders")],
 
1077     ["vendor_invoice_edit",            $locale->text("Create and edit vendor invoices")],
 
1078     ["show_ap_transactions",           $locale->text("Show AP transactions as part of AP invoice report")],
 
1079     ["--warehouse_management",         $locale->text("Warehouse management")],
 
1080     ["warehouse_contents",             $locale->text("View warehouse content")],
 
1081     ["warehouse_management",           $locale->text("Warehouse management")],
 
1082     ["--general_ledger_cash",          $locale->text("General ledger and cash")],
 
1083     ["general_ledger",                 $locale->text("Transactions, AR transactions, AP transactions")],
 
1084     ["datev_export",                   $locale->text("DATEV Export")],
 
1085     ["cash",                           $locale->text("Receipt, payment, reconciliation")],
 
1086     ["--reports",                      $locale->text('Reports')],
 
1087     ["report",                         $locale->text('All reports')],
 
1088     ["advance_turnover_tax_return",    $locale->text('Advance turnover tax return')],
 
1089     ["--batch_printing",               $locale->text("Batch Printing")],
 
1090     ["batch_printing",                 $locale->text("Batch Printing")],
 
1091     ["--configuration",                $locale->text("Configuration")],
 
1092     ["config",                         $locale->text("Change kivitendo installation settings (most entries in the 'System' menu)")],
 
1093     ["admin",                          $locale->text("Client administration: configuration, editing templates, task server control, background jobs (remaining entries in the 'System' menu)")],
 
1094     ["--others",                       $locale->text("Others")],
 
1095     ["email_bcc",                      $locale->text("May set the BCC field when sending emails")],
 
1096     ["productivity",                   $locale->text("Productivity")],
 
1097     ["display_admin_link",             $locale->text("Show administration link")],
 
1104   return grep !/^--/, map { $_->[0] } all_rights_full();
 
1108   $main::lxdebug->enter_sub();
 
1112   my $form   = $main::form;
 
1114   my $dbh    = $self->dbconnect();
 
1116   my $query  = 'SELECT * FROM auth."group"';
 
1117   my $sth    = prepare_execute_query($form, $dbh, $query);
 
1121   while ($row = $sth->fetchrow_hashref()) {
 
1122     $groups->{$row->{id}} = $row;
 
1126   $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
 
1127   $sth   = prepare_query($form, $dbh, $query);
 
1129   foreach $group (values %{$groups}) {
 
1132     do_statement($form, $sth, $query, $group->{id});
 
1134     while ($row = $sth->fetchrow_hashref()) {
 
1135       push @members, $row->{user_id};
 
1137     $group->{members} = [ uniq @members ];
 
1141   $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
 
1142   $sth   = prepare_query($form, $dbh, $query);
 
1144   foreach $group (values %{$groups}) {
 
1145     $group->{rights} = {};
 
1147     do_statement($form, $sth, $query, $group->{id});
 
1149     while ($row = $sth->fetchrow_hashref()) {
 
1150       $group->{rights}->{$row->{right}} |= $row->{granted};
 
1153     map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } all_rights();
 
1157   $main::lxdebug->leave_sub();
 
1163   $main::lxdebug->enter_sub();
 
1168   my $form  = $main::form;
 
1169   my $dbh   = $self->dbconnect();
 
1173   my ($query, $sth, $row, $rights);
 
1175   if (!$group->{id}) {
 
1176     ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
 
1178     $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
 
1179     do_query($form, $dbh, $query, $group->{id});
 
1182   do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
 
1184   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
 
1186   $query  = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
 
1187   $sth    = prepare_query($form, $dbh, $query);
 
1189   foreach my $user_id (uniq @{ $group->{members} }) {
 
1190     do_statement($form, $sth, $query, $user_id, $group->{id});
 
1194   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
 
1196   $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
 
1197   $sth   = prepare_query($form, $dbh, $query);
 
1199   foreach my $right (keys %{ $group->{rights} }) {
 
1200     do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
 
1206   $main::lxdebug->leave_sub();
 
1210   $main::lxdebug->enter_sub();
 
1215   my $form = $main::form;
 
1217   my $dbh  = $self->dbconnect();
 
1220   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
 
1221   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
 
1222   do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
 
1226   $main::lxdebug->leave_sub();
 
1229 sub evaluate_rights_ary {
 
1230   $main::lxdebug->enter_sub(2);
 
1237   foreach my $el (@{$ary}) {
 
1238     if (ref $el eq "ARRAY") {
 
1239       if ($action eq '|') {
 
1240         $value |= evaluate_rights_ary($el);
 
1242         $value &= evaluate_rights_ary($el);
 
1245     } elsif (($el eq '&') || ($el eq '|')) {
 
1248     } elsif ($action eq '|') {
 
1257   $main::lxdebug->leave_sub(2);
 
1262 sub _parse_rights_string {
 
1263   $main::lxdebug->enter_sub(2);
 
1273   push @stack, $cur_ary;
 
1275   while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
 
1277     substr($access, 0, length $1) = "";
 
1279     next if ($token =~ /\s/);
 
1281     if ($token eq "(") {
 
1282       my $new_cur_ary = [];
 
1283       push @stack, $new_cur_ary;
 
1284       push @{$cur_ary}, $new_cur_ary;
 
1285       $cur_ary = $new_cur_ary;
 
1287     } elsif ($token eq ")") {
 
1291         $main::lxdebug->leave_sub(2);
 
1295       $cur_ary = $stack[-1];
 
1297     } elsif (($token eq "|") || ($token eq "&")) {
 
1298       push @{$cur_ary}, $token;
 
1301       push @{$cur_ary}, $self->{RIGHTS}->{$login}->{$token} * 1;
 
1305   my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
 
1307   $main::lxdebug->leave_sub(2);
 
1313   $main::lxdebug->enter_sub(2);
 
1318   my $default = shift;
 
1320   $self->{FULL_RIGHTS}           ||= { };
 
1321   $self->{FULL_RIGHTS}->{$login} ||= { };
 
1323   if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
 
1324     $self->{RIGHTS}           ||= { };
 
1325     $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
 
1327     $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
 
1330   my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
 
1331   $granted    = $default if (!defined $granted);
 
1333   $main::lxdebug->leave_sub(2);
 
1339   $::lxdebug->enter_sub(2);
 
1340   my ($self, $right, $dont_abort) = @_;
 
1342   if ($self->check_right($::myconfig{login}, $right)) {
 
1343     $::lxdebug->leave_sub(2);
 
1348     delete $::form->{title};
 
1349     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
 
1352   $::lxdebug->leave_sub(2);
 
1357 sub load_rights_for_user {
 
1358   $::lxdebug->enter_sub;
 
1360   my ($self, $login) = @_;
 
1361   my $dbh   = $self->dbconnect;
 
1362   my ($query, $sth, $row, $rights);
 
1364   $rights = { map { $_ => 0 } all_rights() };
 
1367     qq|SELECT gr."right", gr.granted
 
1368        FROM auth.group_rights gr
 
1371           FROM auth.user_group ug
 
1372           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
 
1376           FROM auth.clients_groups cg
 
1377           WHERE cg.client_id = ?)|;
 
1379   $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
 
1381   while ($row = $sth->fetchrow_hashref()) {
 
1382     $rights->{$row->{right}} |= $row->{granted};
 
1386   $::lxdebug->leave_sub;
 
1400 SL::Auth - Authentication and session handling
 
1406 =item C<set_session_value @values>
 
1408 =item C<set_session_value %values>
 
1410 Store all values of C<@values> or C<%values> in the session. Each
 
1411 member of C<@values> is tested if it is a hash reference. If it is
 
1412 then it must contain the keys C<key> and C<value> and can optionally
 
1413 contain the key C<auto_restore>. In this case C<value> is associated
 
1414 with C<key> and restored to C<$::form> upon the next request
 
1415 automatically if C<auto_restore> is trueish or if C<value> is a scalar
 
1418 If the current member of C<@values> is not a hash reference then it
 
1419 will be used as the C<key> and the next entry of C<@values> is used as
 
1420 the C<value> to store. In this case setting C<auto_restore> is not
 
1423 Therefore the following two invocations are identical:
 
1425   $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
 
1426   $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
 
1428 All of these values are copied back into C<$::form> for the next
 
1429 request automatically if they're scalar values or if they have
 
1430 C<auto_restore> set to trueish.
 
1432 The values can be any Perl structure. They are stored as YAML dumps.
 
1434 =item C<get_session_value $key>
 
1436 Retrieve a value from the session. Returns C<undef> if the value
 
1439 =item C<create_unique_sesion_value $value, %params>
 
1441 Create a unique key in the session and store C<$value>
 
1444 Returns the key created in the session.
 
1446 =item C<save_session>
 
1448 Stores the session values in the database. This is the only function
 
1449 that actually stores stuff in the database. Neither the various
 
1450 setters nor the deleter access the database.
 
1452 =item <save_form_in_session %params>
 
1454 Stores the content of C<$params{form}> (default: C<$::form>) in the
 
1455 session using L</create_unique_sesion_value>.
 
1457 If C<$params{non_scalars}> is trueish then non-scalar values will be
 
1458 stored as well. Default is to only store scalar values.
 
1460 The following keys will never be saved: C<login>, C<password>,
 
1461 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
 
1462 can be given as an array ref in C<$params{skip_keys}>.
 
1464 Returns the unique key under which the form is stored.
 
1466 =item <restore_form_from_session $key, %params>
 
1468 Restores the form from the session into C<$params{form}> (default:
 
1471 If C<$params{clobber}> is falsish then existing values with the same
 
1472 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
 
1485 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>