5 use Digest::MD5 qw(md5_hex);
 
   7 use Time::HiRes qw(gettimeofday);
 
   8 use List::MoreUtils qw(uniq);
 
  10 use Regexp::IPv6 qw($IPv6_re);
 
  12 use SL::Auth::ColumnInformation;
 
  13 use SL::Auth::Constants qw(:all);
 
  16 use SL::Auth::Password;
 
  17 use SL::Auth::SessionValue;
 
  23 use SL::DBUtils qw(do_query do_statement prepare_execute_query prepare_query selectall_array_query selectrow_query);
 
  27 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
 
  28 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
 
  30 use Rose::Object::MakeMethods::Generic (
 
  31   scalar => [ qw(client) ],
 
  36   my ($type, %params) = @_;
 
  37   my $self            = bless {}, $type;
 
  39   $self->_read_auth_config(%params);
 
  46   my ($self, %params) = @_;
 
  48   $self->{SESSION}            = { };
 
  49   $self->{FULL_RIGHTS}        = { };
 
  50   $self->{RIGHTS}             = { };
 
  51   $self->{unique_counter}     = 0;
 
  52   $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
 
  56   my ($self, %params) = @_;
 
  58   $self->{SESSION}        = { };
 
  59   $self->{FULL_RIGHTS}    = { };
 
  60   $self->{RIGHTS}         = { };
 
  61   $self->{unique_counter} = 0;
 
  63   if ($self->is_db_connected) {
 
  64     # reset is called during request shutdown already. In case of a
 
  65     # completely new auth DB this would fail and generate an error
 
  66     # message even if the user is currently trying to create said auth
 
  67     # DB. Therefore only fetch the column information if a connection
 
  68     # has been established.
 
  69     $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
 
  70     $self->{column_information}->_fetch;
 
  72     delete $self->{column_information};
 
  75   $self->{authenticator}->reset;
 
  81   my ($self, $id_or_name) = @_;
 
  85   return undef unless $id_or_name;
 
  87   my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
 
  88   my $dbh    = $self->dbconnect;
 
  90   return undef unless $dbh;
 
  92   $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
 
 100   $self->{dbh}->disconnect() if ($self->{dbh});
 
 103 # form isn't loaded yet, so auth needs it's own error.
 
 105   $::lxdebug->show_backtrace();
 
 107   my ($self, @msg) = @_;
 
 108   if ($ENV{HTTP_USER_AGENT}) {
 
 109     print Form->create_http_response(content_type => 'text/html');
 
 110     print "<pre>", join ('<br>', @msg), "</pre>";
 
 112     print STDERR "Error: @msg\n";
 
 114   $::dispatcher->end_request;
 
 117 sub _read_auth_config {
 
 118   my ($self, %params) = @_;
 
 120   map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
 
 122   # Prevent password leakage to log files when dumping Auth instances.
 
 123   $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
 
 125   if ($params{unit_tests_database}) {
 
 126     $self->{DB_config}   = $::lx_office_conf{'testing/database'};
 
 127     $self->{module}      = 'DB';
 
 130     $self->{DB_config}   = $::lx_office_conf{'authentication/database'};
 
 131     $self->{LDAP_config} = $::lx_office_conf{'authentication/ldap'};
 
 134   if ($self->{module} eq 'DB') {
 
 135     $self->{authenticator} = SL::Auth::DB->new($self);
 
 137   } elsif ($self->{module} eq 'LDAP') {
 
 138     $self->{authenticator} = SL::Auth::LDAP->new($self);
 
 141   if (!$self->{authenticator}) {
 
 142     my $locale = Locale->new('en');
 
 143     $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
 
 146   my $cfg = $self->{DB_config};
 
 149     my $locale = Locale->new('en');
 
 150     $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
 
 153   if (!$cfg->{host} || !$cfg->{db} || !$cfg->{user}) {
 
 154     my $locale = Locale->new('en');
 
 155     $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
 
 158   $self->{authenticator}->verify_config();
 
 160   $self->{session_timeout} *= 1;
 
 161   $self->{session_timeout}  = 8 * 60 if (!$self->{session_timeout});
 
 164 sub has_access_to_client {
 
 165   my ($self, $login) = @_;
 
 167   return 0 if !$self->client || !$self->client->{id};
 
 171     FROM auth.clients_users cu
 
 172     LEFT JOIN auth."user" u ON (cu.user_id = u.id)
 
 174       AND (cu.client_id = ?)
 
 177   my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
 
 181 sub authenticate_root {
 
 182   my ($self, $password) = @_;
 
 184   my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
 
 185   if (defined $session_root_auth && $session_root_auth == OK) {
 
 189   if (!defined $password) {
 
 193   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
 
 194   $password             = SL::Auth::Password->hash(login => 'root', password => $password, stored_password => $admin_password);
 
 196   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
 
 197   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
 
 203   my ($self, $login, $password) = @_;
 
 205   if (!$self->client || !$self->has_access_to_client($login)) {
 
 209   my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
 
 210   if (defined $session_auth && $session_auth == OK) {
 
 214   if (!defined $password) {
 
 218   my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
 
 219   $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
 
 223 sub punish_wrong_login {
 
 224   my $failed_login_penalty = ($::lx_office_conf{authentication} || {})->{failed_login_penalty};
 
 225   sleep $failed_login_penalty if $failed_login_penalty;
 
 228 sub get_stored_password {
 
 229   my ($self, $login) = @_;
 
 231   my $dbh            = $self->dbconnect;
 
 233   return undef unless $dbh;
 
 235   my $query             = qq|SELECT password FROM auth."user" WHERE login = ?|;
 
 236   my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
 
 238   return $stored_password;
 
 243   my $may_fail = shift;
 
 249   my $cfg = $self->{DB_config};
 
 250   my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
 
 253     $dsn .= ';port=' . $cfg->{port};
 
 256   $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
 
 258   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
 
 260   if (!$may_fail && !$self->{dbh}) {
 
 262     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
 
 272     $self->{dbh}->disconnect();
 
 277 sub is_db_connected {
 
 279   return !!$self->{dbh};
 
 283   my ($self, $dbh)    = @_;
 
 285   $dbh   ||= $self->dbconnect();
 
 286   my $query   = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
 
 288   my ($count) = $dbh->selectrow_array($query);
 
 296   my $dbh  = $self->dbconnect(1);
 
 301 sub create_database {
 
 305   my $cfg    = $self->{DB_config};
 
 307   if (!$params{superuser}) {
 
 308     $params{superuser}          = $cfg->{user};
 
 309     $params{superuser_password} = $cfg->{password};
 
 312   $params{template} ||= 'template0';
 
 313   $params{template}   =~ s|[^a-zA-Z0-9_\-]||g;
 
 315   my $dsn = 'dbi:Pg:dbname=template1;host=' . $cfg->{host};
 
 318     $dsn .= ';port=' . $cfg->{port};
 
 321   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database DSN: $dsn");
 
 323   my $dbh = SL::DBConnect->connect($dsn, $params{superuser}, $params{superuser_password}, { pg_enable_utf8 => 1 });
 
 326     $main::form->error($main::locale->text('The connection to the template database failed:') . "\n" . $DBI::errstr);
 
 329   my $query = qq|CREATE DATABASE "$cfg->{db}" OWNER "$cfg->{user}" TEMPLATE "$params{template}" ENCODING 'UNICODE'|;
 
 331   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database query: $query");
 
 336     my $error = $dbh->errstr();
 
 338     $query                 = qq|SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'|;
 
 339     my ($cluster_encoding) = $dbh->selectrow_array($query);
 
 341     if ($cluster_encoding && ($cluster_encoding !~ m/^(?:UTF-?8|UNICODE)$/i)) {
 
 342       $error = $::locale->text('Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.');
 
 347     $main::form->error($main::locale->text('The creation of the authentication database failed:') . "\n" . $error);
 
 355   my $dbh  = $self->dbconnect();
 
 358   SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
 
 366   my $form   = $main::form;
 
 368   my $dbh    = $self->dbconnect();
 
 370   my ($sth, $query, $user_id);
 
 374   $query     = qq|SELECT id FROM auth."user" WHERE login = ?|;
 
 375   ($user_id) = selectrow_query($form, $dbh, $query, $login);
 
 378     $query     = qq|SELECT nextval('auth.user_id_seq')|;
 
 379     ($user_id) = selectrow_query($form, $dbh, $query);
 
 381     $query     = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
 
 382     do_query($form, $dbh, $query, $user_id, $login);
 
 385   $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
 
 386   do_query($form, $dbh, $query, $user_id);
 
 388   $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
 
 389   $sth   = prepare_query($form, $dbh, $query);
 
 391   while (my ($cfg_key, $cfg_value) = each %params) {
 
 392     next if ($cfg_key eq 'password');
 
 394     do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
 
 400 sub can_change_password {
 
 403   return $self->{authenticator}->can_change_password();
 
 406 sub change_password {
 
 407   my ($self, $login, $new_password) = @_;
 
 409   my $result = $self->{authenticator}->change_password($login, $new_password);
 
 417   my $dbh   = $self->dbconnect();
 
 418   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value, s.mtime AS last_action
 
 420                  FROM auth."user" AS  u
 
 422                  LEFT JOIN auth.user_config AS cfg
 
 423                    ON (cfg.user_id = u.id)
 
 425                  LEFT JOIN auth.session_content AS sc_login
 
 426                    ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
 
 428                  LEFT JOIN auth.session AS s
 
 429                    ON (s.id = sc_login.session_id)
 
 431   my $sth   = prepare_execute_query($main::form, $dbh, $query);
 
 435   while (my $ref = $sth->fetchrow_hashref()) {
 
 437     $users{$ref->{login}}                    ||= {
 
 438                                                 'login' => $ref->{login},
 
 440                                                 'last_action' => $ref->{last_action},
 
 442     $users{$ref->{login}}->{$ref->{cfg_key}}   = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
 
 451   my ($self, %params) = @_;
 
 453   my $dbh   = $self->dbconnect();
 
 455   my (@where, @values);
 
 456   if ($params{login}) {
 
 457     push @where,  'u.login = ?';
 
 458     push @values, $params{login};
 
 461     push @where,  'u.id = ?';
 
 462     push @values, $params{id};
 
 464   my $where = join ' AND ', '1 = 1', @where;
 
 465   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
 
 466                  FROM auth.user_config cfg
 
 467                  LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
 
 469   my $sth   = prepare_execute_query($main::form, $dbh, $query, @values);
 
 473   while (my $ref = $sth->fetchrow_hashref()) {
 
 474     $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
 
 475     @user_data{qw(id login)}    = @{$ref}{qw(id login)};
 
 478   # The XUL/XML & 'CSS new' backed menus have been removed.
 
 479   my %menustyle_map = ( xml => 'new', v4 => 'v3' );
 
 480   $user_data{menustyle} = $menustyle_map{lc($user_data{menustyle} || '')} || $user_data{menustyle};
 
 482   # The 'Win2000.css' stylesheet has been removed.
 
 483   $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
 
 485   # Set default language if selected language does not exist (anymore).
 
 486   $user_data{countrycode} = $::lx_office_conf{system}->{language} unless $user_data{countrycode} && -d "locale/$user_data{countrycode}";
 
 497   my $dbh   = $self->dbconnect();
 
 498   my ($id)  = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
 
 507   my $dbh   = $self->dbconnect;
 
 508   my $id    = $self->get_user_id($login);
 
 517   do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
 
 518   do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
 
 519   do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
 
 521   # TODO: SL::Auth::delete_user
 
 522   # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
 
 527 # --------------------------------------
 
 531 sub restore_session {
 
 534   $session_id        =  $::request->{cgi}->cookie($self->get_session_cookie_name());
 
 535   $session_id        =~ s|[^0-9a-f]||g if $session_id;
 
 537   $self->{SESSION}   = { };
 
 540     return $self->session_restore_result(SESSION_NONE());
 
 543   my ($dbh, $query, $sth, $cookie, $ref, $form);
 
 547   # Don't fail if the auth DB doesn't exist yet.
 
 548   if (!( $dbh = $self->dbconnect(1) )) {
 
 549     return $self->session_restore_result(SESSION_NONE());
 
 552   # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
 
 553   # admin is creating the session tables at the moment.
 
 554   $query  = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
 
 556   if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
 
 557     $sth->finish if $sth;
 
 558     return $self->session_restore_result(SESSION_NONE());
 
 561   $cookie = $sth->fetchrow_hashref;
 
 564   # The session ID provided is valid in the following cases:
 
 565   #  1. session ID exists in the database
 
 566   #  2. hasn't expired yet
 
 567   #  3. if cookie for the API token is given: the cookie's value equal database column 'auth.session.api_token' for the session ID
 
 568   #  4. if cookie for the API token is NOT given then: the requestee's IP address must match the stored IP address
 
 569   $self->{api_token}   = $cookie->{api_token} if $cookie;
 
 570   my $api_token_cookie = $self->get_api_token_cookie;
 
 571   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
 
 572   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
 
 573   $cookie_is_bad     ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR}                       if !$api_token_cookie && $ENV{REMOTE_ADDR} !~ /^$IPv6_re$/;
 
 574   if ($cookie_is_bad) {
 
 575     $self->destroy_session();
 
 576     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
 
 579   if ($self->{column_information}->has('auto_restore')) {
 
 580     $self->_load_with_auto_restore_column($dbh, $session_id);
 
 582     $self->_load_without_auto_restore_column($dbh, $session_id);
 
 585   return $self->session_restore_result(SESSION_OK());
 
 588 sub session_restore_result {
 
 591     $self->{session_restore_result} = $_[0];
 
 593   return $self->{session_restore_result};
 
 596 sub _load_without_auto_restore_column {
 
 597   my ($self, $dbh, $session_id) = @_;
 
 600     SELECT sess_key, sess_value
 
 601     FROM auth.session_content
 
 602     WHERE (session_id = ?)
 
 604   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
 606   while (my $ref = $sth->fetchrow_hashref) {
 
 607     my $value = SL::Auth::SessionValue->new(auth  => $self,
 
 608                                             key   => $ref->{sess_key},
 
 609                                             value => $ref->{sess_value},
 
 611     $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 613     next if defined $::form->{$ref->{sess_key}};
 
 615     my $data                    = $value->get;
 
 616     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
 
 620 sub _load_with_auto_restore_column {
 
 621   my ($self, $dbh, $session_id) = @_;
 
 623   my %auto_restore_keys = map { $_ => 1 } qw(login password rpw client_id), SESSION_KEY_ROOT_AUTH, SESSION_KEY_USER_AUTH;
 
 626     SELECT sess_key, sess_value, auto_restore
 
 627     FROM auth.session_content
 
 628     WHERE (session_id = ?)
 
 630   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
 632   while (my $ref = $sth->fetchrow_hashref) {
 
 633     if ($ref->{auto_restore} || $auto_restore_keys{$ref->{sess_key}}) {
 
 634       my $value = SL::Auth::SessionValue->new(auth         => $self,
 
 635                                               key          => $ref->{sess_key},
 
 636                                               value        => $ref->{sess_value},
 
 637                                               auto_restore => $ref->{auto_restore},
 
 639       $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 641       next if defined $::form->{$ref->{sess_key}};
 
 643       my $data                    = $value->get;
 
 644       $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
 
 646       my $value = SL::Auth::SessionValue->new(auth => $self,
 
 647                                               key  => $ref->{sess_key});
 
 648       $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 655 sub destroy_session {
 
 659     my $dbh = $self->dbconnect();
 
 663     do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
 
 664     do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
 
 668     SL::SessionFile->destroy_session($session_id);
 
 671     $self->{SESSION} = { };
 
 675 sub active_session_ids {
 
 677   my $dbh   = $self->dbconnect;
 
 679   my $query = qq|SELECT id FROM auth.session|;
 
 681   my @ids   = selectall_array_query($::form, $dbh, $query);
 
 686 sub expire_sessions {
 
 689   return if !$self->session_tables_present;
 
 691   my $dbh   = $self->dbconnect();
 
 693   my $query = qq|SELECT id
 
 695                  WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
 
 697   my @ids   = selectall_array_query($::form, $dbh, $query);
 
 702     SL::SessionFile->destroy_session($_) for @ids;
 
 704     $query = qq|DELETE FROM auth.session_content
 
 705                 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
 
 706     do_query($main::form, $dbh, $query, @ids);
 
 708     $query = qq|DELETE FROM auth.session
 
 709                 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
 
 710     do_query($main::form, $dbh, $query, @ids);
 
 716 sub _create_session_id {
 
 718   map { push @data, int(rand() * 255); } (1..32);
 
 720   my $id = md5_hex(pack 'C*', @data);
 
 725 sub create_or_refresh_session {
 
 726   $session_id ||= shift->_create_session_id;
 
 731   my $provided_dbh = shift;
 
 733   my $dbh          = $provided_dbh || $self->dbconnect(1);
 
 735   return unless $dbh && $session_id;
 
 737   $dbh->begin_work unless $provided_dbh;
 
 739   # If this fails then the "auth" schema might not exist yet, e.g. if
 
 740   # the admin is just trying to create the auth database.
 
 741   if (!$dbh->do(qq|LOCK auth.session_content|)) {
 
 742     $dbh->rollback unless $provided_dbh;
 
 746   my @unfetched_keys = map     { $_->{key}        }
 
 747                        grep    { ! $_->{fetched}  }
 
 748                        values %{ $self->{SESSION} };
 
 749   # $::lxdebug->dump(0, "unfetched_keys", [ sort @unfetched_keys ]);
 
 750   # $::lxdebug->dump(0, "all keys", [ sort map { $_->{key} } values %{ $self->{SESSION} } ]);
 
 751   my $query          = qq|DELETE FROM auth.session_content WHERE (session_id = ?)|;
 
 752   $query            .= qq| AND (sess_key NOT IN (| . join(', ', ('?') x scalar @unfetched_keys) . qq|))| if @unfetched_keys;
 
 754   do_query($::form, $dbh, $query, $session_id, @unfetched_keys);
 
 756   my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
 
 759     do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
 
 761     do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
 
 764   if ($self->{column_information}->has('api_token', 'session')) {
 
 765     my ($stored_api_token) = $dbh->selectrow_array(qq|SELECT api_token FROM auth.session WHERE id = ?|, undef, $session_id);
 
 766     do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
 
 769   my @values_to_save = grep    { $_->{fetched} }
 
 770                        values %{ $self->{SESSION} };
 
 771   if (@values_to_save) {
 
 772     my ($columns, $placeholders) = ('', '');
 
 773     my $auto_restore             = $self->{column_information}->has('auto_restore');
 
 776       $columns      .= ', auto_restore';
 
 777       $placeholders .= ', ?';
 
 780     $query  = qq|INSERT INTO auth.session_content (session_id, sess_key, sess_value ${columns}) VALUES (?, ?, ? ${placeholders})|;
 
 781     my $sth = prepare_query($::form, $dbh, $query);
 
 783     foreach my $value (@values_to_save) {
 
 784       my @values = ($value->{key}, $value->get_dumped);
 
 785       push @values, $value->{auto_restore} if $auto_restore;
 
 787       do_statement($::form, $sth, $query, $session_id, @values);
 
 793   $dbh->commit() unless $provided_dbh;
 
 796 sub set_session_value {
 
 800   $self->{SESSION} ||= { };
 
 803     my $key = shift @params;
 
 805     if (ref $key eq 'HASH') {
 
 806       $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key          => $key->{key},
 
 807                                                                       value        => $key->{value},
 
 808                                                                       auto_restore => $key->{auto_restore});
 
 811       my $value = shift @params;
 
 812       $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key   => $key,
 
 820 sub delete_session_value {
 
 823   $self->{SESSION} ||= { };
 
 824   delete @{ $self->{SESSION} }{ @_ };
 
 829 sub get_session_value {
 
 831   my $data = $self->{SESSION} && $self->{SESSION}->{ $_[0] } ? $self->{SESSION}->{ $_[0] }->get : undef;
 
 836 sub create_unique_sesion_value {
 
 837   my ($self, $value, %params) = @_;
 
 839   $self->{SESSION} ||= { };
 
 841   my @now                   = gettimeofday();
 
 842   my $key                   = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
 
 843   $self->{unique_counter} ||= 0;
 
 847     $self->{unique_counter}++;
 
 848     $hashed_key = md5_hex($key . $self->{unique_counter});
 
 849   } while (exists $self->{SESSION}->{$hashed_key});
 
 851   $self->set_session_value($hashed_key => $value);
 
 856 sub save_form_in_session {
 
 857   my ($self, %params) = @_;
 
 859   my $form        = delete($params{form}) || $::form;
 
 860   my $non_scalars = delete $params{non_scalars};
 
 863   my %skip_keys   = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
 
 865   foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
 
 866     $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
 
 869   return $self->create_unique_sesion_value($data, %params);
 
 872 sub restore_form_from_session {
 
 873   my ($self, $key, %params) = @_;
 
 875   my $data = $self->get_session_value($key);
 
 876   return $self unless $data;
 
 878   my $form    = delete($params{form}) || $::form;
 
 879   my $clobber = exists $params{clobber} ? $params{clobber} : 1;
 
 881   map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
 
 886 sub set_cookie_environment_variable {
 
 888   $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
 
 891 sub get_session_cookie_name {
 
 892   my ($self, %params) = @_;
 
 894   $params{type}     ||= 'id';
 
 895   my $name            = $self->{cookie_name} || 'lx_office_erp_session_id';
 
 896   $name              .= '_api_token' if $params{type} eq 'api_token';
 
 905 sub get_api_token_cookie {
 
 908   $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
 
 911 sub is_api_token_cookie_valid {
 
 913   my $provided_api_token = $self->get_api_token_cookie;
 
 914   return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
 
 917 sub _tables_present {
 
 918   my ($self, @tables) = @_;
 
 919   my $cache_key = join '_', @tables;
 
 921   # Only re-check for the presence of auth tables if either the check
 
 922   # hasn't been done before of if they weren't present.
 
 923   return $self->{"$cache_key\_tables_present"} ||= do {
 
 924     my $dbh  = $self->dbconnect(1);
 
 933          WHERE (schemaname = 'auth')
 
 934            AND (tablename IN (@{[ join ', ', ('?') x @tables ]}))|;
 
 936     my ($count) = selectrow_query($main::form, $dbh, $query, @tables);
 
 938     scalar @tables == $count;
 
 942 sub session_tables_present {
 
 943   $_[0]->_tables_present('session', 'session_content');
 
 946 sub master_rights_present {
 
 947   $_[0]->_tables_present('master_rights');
 
 950 # --------------------------------------
 
 952 sub all_rights_full {
 
 955   @{ $self->{master_rights} ||= do {
 
 956       $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY position");
 
 962   return map { $_->[0] } grep { !$_->[2] } $_[0]->all_rights_full;
 
 968   my $form   = $main::form;
 
 970   my $dbh    = $self->dbconnect();
 
 972   my $query  = 'SELECT * FROM auth."group"';
 
 973   my $sth    = prepare_execute_query($form, $dbh, $query);
 
 977   while ($row = $sth->fetchrow_hashref()) {
 
 978     $groups->{$row->{id}} = $row;
 
 982   $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
 
 983   $sth   = prepare_query($form, $dbh, $query);
 
 985   foreach $group (values %{$groups}) {
 
 988     do_statement($form, $sth, $query, $group->{id});
 
 990     while ($row = $sth->fetchrow_hashref()) {
 
 991       push @members, $row->{user_id};
 
 993     $group->{members} = [ uniq @members ];
 
 997   $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
 
 998   $sth   = prepare_query($form, $dbh, $query);
 
1000   foreach $group (values %{$groups}) {
 
1001     $group->{rights} = {};
 
1003     do_statement($form, $sth, $query, $group->{id});
 
1005     while ($row = $sth->fetchrow_hashref()) {
 
1006       $group->{rights}->{$row->{right}} |= $row->{granted};
 
1009     map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } $self->all_rights;
 
1020   my $form  = $main::form;
 
1021   my $dbh   = $self->dbconnect();
 
1025   my ($query, $sth, $row, $rights);
 
1027   if (!$group->{id}) {
 
1028     ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
 
1030     $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
 
1031     do_query($form, $dbh, $query, $group->{id});
 
1034   do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
 
1036   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
 
1038   $query  = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
 
1039   $sth    = prepare_query($form, $dbh, $query);
 
1041   foreach my $user_id (uniq @{ $group->{members} }) {
 
1042     do_statement($form, $sth, $query, $user_id, $group->{id});
 
1046   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
 
1048   $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
 
1049   $sth   = prepare_query($form, $dbh, $query);
 
1051   foreach my $right (keys %{ $group->{rights} }) {
 
1052     do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
 
1063   my $form = $main::form;
 
1065   my $dbh  = $self->dbconnect();
 
1068   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
 
1069   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
 
1070   do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
 
1075 sub evaluate_rights_ary {
 
1081   foreach my $el (@{$ary}) {
 
1082     if (ref $el eq "ARRAY") {
 
1083       if ($action eq '|') {
 
1084         $value |= evaluate_rights_ary($el);
 
1086         $value &= evaluate_rights_ary($el);
 
1089     } elsif (($el eq '&') || ($el eq '|')) {
 
1092     } elsif ($action eq '|') {
 
1104 sub _parse_rights_string {
 
1113   push @stack, $cur_ary;
 
1115   while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
 
1117     substr($access, 0, length $1) = "";
 
1119     next if ($token =~ /\s/);
 
1121     if ($token eq "(") {
 
1122       my $new_cur_ary = [];
 
1123       push @stack, $new_cur_ary;
 
1124       push @{$cur_ary}, $new_cur_ary;
 
1125       $cur_ary = $new_cur_ary;
 
1127     } elsif ($token eq ")") {
 
1134       $cur_ary = $stack[-1];
 
1136     } elsif (($token eq "|") || ($token eq "&")) {
 
1137       push @{$cur_ary}, $token;
 
1140       push @{$cur_ary}, ($self->{RIGHTS}->{$login}->{$token} // 0) * 1;
 
1144   my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
 
1153   my $default = shift;
 
1155   $self->{FULL_RIGHTS}           ||= { };
 
1156   $self->{FULL_RIGHTS}->{$login} ||= { };
 
1158   if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
 
1159     $self->{RIGHTS}           ||= { };
 
1160     $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
 
1162     $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
 
1165   my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
 
1166   $granted    = $default if (!defined $granted);
 
1172   my ($self, $right, $dont_abort) = @_;
 
1174   if ($self->check_right($::myconfig{login}, $right)) {
 
1179     delete $::form->{title};
 
1180     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
 
1186 sub load_rights_for_user {
 
1187   my ($self, $login) = @_;
 
1188   my $dbh   = $self->dbconnect;
 
1189   my ($query, $sth, $row, $rights);
 
1191   $rights = { map { $_ => 0 } $self->all_rights };
 
1193   return $rights if !$self->client || !$login;
 
1196     qq|SELECT gr."right", gr.granted
 
1197        FROM auth.group_rights gr
 
1200           FROM auth.user_group ug
 
1201           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
 
1205           FROM auth.clients_groups cg
 
1206           WHERE cg.client_id = ?)|;
 
1208   $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
 
1210   while ($row = $sth->fetchrow_hashref()) {
 
1211     $rights->{$row->{right}} |= $row->{granted};
 
1227 SL::Auth - Authentication and session handling
 
1233 =item C<set_session_value @values>
 
1235 =item C<set_session_value %values>
 
1237 Store all values of C<@values> or C<%values> in the session. Each
 
1238 member of C<@values> is tested if it is a hash reference. If it is
 
1239 then it must contain the keys C<key> and C<value> and can optionally
 
1240 contain the key C<auto_restore>. In this case C<value> is associated
 
1241 with C<key> and restored to C<$::form> upon the next request
 
1242 automatically if C<auto_restore> is trueish or if C<value> is a scalar
 
1245 If the current member of C<@values> is not a hash reference then it
 
1246 will be used as the C<key> and the next entry of C<@values> is used as
 
1247 the C<value> to store. In this case setting C<auto_restore> is not
 
1250 Therefore the following two invocations are identical:
 
1252   $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
 
1253   $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
 
1255 All of these values are copied back into C<$::form> for the next
 
1256 request automatically if they're scalar values or if they have
 
1257 C<auto_restore> set to trueish.
 
1259 The values can be any Perl structure. They are stored as YAML dumps.
 
1261 =item C<get_session_value $key>
 
1263 Retrieve a value from the session. Returns C<undef> if the value
 
1266 =item C<create_unique_sesion_value $value, %params>
 
1268 Create a unique key in the session and store C<$value>
 
1271 Returns the key created in the session.
 
1273 =item C<save_session>
 
1275 Stores the session values in the database. This is the only function
 
1276 that actually stores stuff in the database. Neither the various
 
1277 setters nor the deleter access the database.
 
1279 =item C<save_form_in_session %params>
 
1281 Stores the content of C<$params{form}> (default: C<$::form>) in the
 
1282 session using L</create_unique_sesion_value>.
 
1284 If C<$params{non_scalars}> is trueish then non-scalar values will be
 
1285 stored as well. Default is to only store scalar values.
 
1287 The following keys will never be saved: C<login>, C<password>,
 
1288 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
 
1289 can be given as an array ref in C<$params{skip_keys}>.
 
1291 Returns the unique key under which the form is stored.
 
1293 =item C<restore_form_from_session $key, %params>
 
1295 Restores the form from the session into C<$params{form}> (default:
 
1298 If C<$params{clobber}> is falsish then existing values with the same
 
1299 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
 
1306 C<reset> deletes every state information from previous requests, but does not
 
1307 close the database connection.
 
1309 Creating a new database handle on each request can take up to 30% of the
 
1310 pre-request startup time, so we want to avoid that for fast ajax calls.
 
1320 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>