5 use Digest::MD5 qw(md5_hex);
 
   7 use Time::HiRes qw(gettimeofday);
 
   8 use List::MoreUtils qw(any 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 selectall_ids);
 
  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   $_->reset for @{ $self->{authenticators} };
 
  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));
 
  97 sub get_default_client_id {
 
 100   my $dbh    = $self->dbconnect;
 
 104   my $row = $dbh->selectrow_hashref(qq|SELECT id FROM auth.clients WHERE is_default = TRUE LIMIT 1|);
 
 106   return $row->{id} if $row;
 
 112   $self->{dbh}->disconnect() if ($self->{dbh});
 
 115 # form isn't loaded yet, so auth needs it's own error.
 
 117   $::lxdebug->show_backtrace();
 
 119   my ($self, @msg) = @_;
 
 120   if ($ENV{HTTP_USER_AGENT}) {
 
 121     # $::form might not be initialized yet at this point — therefore
 
 122     # we cannot use "create_http_response" yet.
 
 123     my $cgi = CGI->new('');
 
 124     print $cgi->header('-type' => 'text/html', '-charset' => 'UTF-8');
 
 125     print "<pre>", join ('<br>', @msg), "</pre>";
 
 127     print STDERR "Error: @msg\n";
 
 129   $::dispatcher->end_request;
 
 132 sub _read_auth_config {
 
 133   my ($self, %params) = @_;
 
 135   map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
 
 137   # Prevent password leakage to log files when dumping Auth instances.
 
 138   $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
 
 140   if ($params{unit_tests_database}) {
 
 141     $self->{DB_config}   = $::lx_office_conf{'testing/database'};
 
 142     $self->{module}      = 'DB';
 
 145     $self->{DB_config}   = $::lx_office_conf{'authentication/database'};
 
 148   $self->{authenticators} =  [];
 
 149   $self->{module}       ||=  'DB';
 
 150   $self->{module}         =~ s{^ +| +$}{}g;
 
 152   foreach my $module (split m{ +}, $self->{module}) {
 
 154     ($module, $config_name) = split m{:}, $module, 2;
 
 155     $config_name          ||= $module eq 'DB' ? 'database' : lc($module);
 
 156     my $config              = $::lx_office_conf{'authentication/' . $config_name};
 
 159       my $locale = Locale->new('en');
 
 160       $self->mini_error($locale->text('Missing configuration section "authentication/#1" in "config/kivitendo.conf".', $config_name));
 
 163     if ($module eq 'DB') {
 
 164       push @{ $self->{authenticators} }, SL::Auth::DB->new($self);
 
 166     } elsif ($module eq 'LDAP') {
 
 167       push @{ $self->{authenticators} }, SL::Auth::LDAP->new($config);
 
 170       my $locale = Locale->new('en');
 
 171       $self->mini_error($locale->text('Unknown authenticantion module #1 specified in "config/kivitendo.conf".', $module));
 
 175   my $cfg = $self->{DB_config};
 
 178     my $locale = Locale->new('en');
 
 179     $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
 
 182   if (!$cfg->{host} || !$cfg->{db} || !$cfg->{user}) {
 
 183     my $locale = Locale->new('en');
 
 184     $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
 
 187   $_->verify_config for @{ $self->{authenticators} };
 
 189   $self->{session_timeout} *= 1;
 
 190   $self->{session_timeout}  = 8 * 60 if (!$self->{session_timeout});
 
 193 sub has_access_to_client {
 
 194   my ($self, $login) = @_;
 
 196   return 0 if !$self->client || !$self->client->{id};
 
 200     FROM auth.clients_users cu
 
 201     LEFT JOIN auth."user" u ON (cu.user_id = u.id)
 
 203       AND (cu.client_id = ?)
 
 206   my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
 
 210 sub authenticate_root {
 
 211   my ($self, $password) = @_;
 
 213   my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
 
 214   if (defined $session_root_auth && $session_root_auth == OK) {
 
 218   if (!defined $password) {
 
 222   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
 
 223   $password             = SL::Auth::Password->hash(login => 'root', password => $password, stored_password => $admin_password);
 
 225   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
 
 226   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
 
 232   my ($self, $login, $password) = @_;
 
 234   if (!$self->client || !$self->has_access_to_client($login)) {
 
 238   my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
 
 239   if (defined $session_auth && $session_auth == OK) {
 
 243   if (!defined $password) {
 
 247   my $result = ERR_USER;
 
 249     foreach my $authenticator (@{ $self->{authenticators} }) {
 
 250       $result = $authenticator->authenticate($login, $password);
 
 251       last if $result == OK;
 
 255   $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
 
 259 sub punish_wrong_login {
 
 260   my $failed_login_penalty = ($::lx_office_conf{authentication} || {})->{failed_login_penalty};
 
 261   sleep $failed_login_penalty if $failed_login_penalty;
 
 264 sub get_stored_password {
 
 265   my ($self, $login) = @_;
 
 267   my $dbh            = $self->dbconnect;
 
 269   return undef unless $dbh;
 
 271   my $query             = qq|SELECT password FROM auth."user" WHERE login = ?|;
 
 272   my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
 
 274   return $stored_password;
 
 279   my $may_fail = shift;
 
 285   my $cfg = $self->{DB_config};
 
 286   my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
 
 289     $dsn .= ';port=' . $cfg->{port};
 
 292   $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
 
 294   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
 
 296   if (!$may_fail && !$self->{dbh}) {
 
 298     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
 
 308     $self->{dbh}->disconnect();
 
 313 sub is_db_connected {
 
 315   return !!$self->{dbh};
 
 319   my ($self, $dbh)    = @_;
 
 321   $dbh   ||= $self->dbconnect();
 
 322   my $query   = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
 
 324   my ($count) = $dbh->selectrow_array($query);
 
 332   my $dbh  = $self->dbconnect(1);
 
 337 sub create_database {
 
 341   my $cfg    = $self->{DB_config};
 
 343   if (!$params{superuser}) {
 
 344     $params{superuser}          = $cfg->{user};
 
 345     $params{superuser_password} = $cfg->{password};
 
 348   $params{template} ||= 'template0';
 
 349   $params{template}   =~ s|[^a-zA-Z0-9_\-]||g;
 
 351   my $dsn = 'dbi:Pg:dbname=template1;host=' . $cfg->{host};
 
 354     $dsn .= ';port=' . $cfg->{port};
 
 357   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database DSN: $dsn");
 
 359   my $dbh = SL::DBConnect->connect($dsn, $params{superuser}, $params{superuser_password}, { pg_enable_utf8 => 1 });
 
 362     $main::form->error($main::locale->text('The connection to the template database failed:') . "\n" . $DBI::errstr);
 
 365   my $query = qq|CREATE DATABASE "$cfg->{db}" OWNER "$cfg->{user}" TEMPLATE "$params{template}" ENCODING 'UNICODE'|;
 
 367   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database query: $query");
 
 372     my $error = $dbh->errstr();
 
 374     $query                 = qq|SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'|;
 
 375     my ($cluster_encoding) = $dbh->selectrow_array($query);
 
 377     if ($cluster_encoding && ($cluster_encoding !~ m/^(?:UTF-?8|UNICODE)$/i)) {
 
 378       $error = $::locale->text('Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.');
 
 383     $main::form->error($main::locale->text('The creation of the authentication database failed:') . "\n" . $error);
 
 391   my $dbh  = $self->dbconnect();
 
 394   SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
 
 402   my $form   = $main::form;
 
 404   my $dbh    = $self->dbconnect();
 
 406   my ($sth, $query, $user_id);
 
 410   $query     = qq|SELECT id FROM auth."user" WHERE login = ?|;
 
 411   ($user_id) = selectrow_query($form, $dbh, $query, $login);
 
 414     $query     = qq|SELECT nextval('auth.user_id_seq')|;
 
 415     ($user_id) = selectrow_query($form, $dbh, $query);
 
 417     $query     = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
 
 418     do_query($form, $dbh, $query, $user_id, $login);
 
 421   $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
 
 422   do_query($form, $dbh, $query, $user_id);
 
 424   $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
 
 425   $sth   = prepare_query($form, $dbh, $query);
 
 427   while (my ($cfg_key, $cfg_value) = each %params) {
 
 428     next if ($cfg_key eq 'password');
 
 430     do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
 
 436 sub can_change_password {
 
 439   return any { $_->can_change_password } @{ $self->{authenticators} };
 
 442 sub change_password {
 
 443   my ($self, $login, $new_password) = @_;
 
 445   my $overall_result = OK;
 
 447   foreach my $authenticator (@{ $self->{authenticators} }) {
 
 448     next unless $authenticator->can_change_password;
 
 450     my $result = $authenticator->change_password($login, $new_password);
 
 451     $overall_result = $result if $result != OK;
 
 454   return $overall_result;
 
 460   my $dbh   = $self->dbconnect();
 
 461   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value, s.mtime AS last_action
 
 463                  FROM auth."user" AS  u
 
 465                  LEFT JOIN auth.user_config AS cfg
 
 466                    ON (cfg.user_id = u.id)
 
 468                  LEFT JOIN auth.session_content AS sc_login
 
 469                    ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
 
 471                  LEFT JOIN auth.session AS s
 
 472                    ON (s.id = sc_login.session_id)
 
 474   my $sth   = prepare_execute_query($main::form, $dbh, $query);
 
 478   while (my $ref = $sth->fetchrow_hashref()) {
 
 480     $users{$ref->{login}}                    ||= {
 
 481                                                 'login' => $ref->{login},
 
 483                                                 'last_action' => $ref->{last_action},
 
 485     $users{$ref->{login}}->{$ref->{cfg_key}}   = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
 
 494   my ($self, %params) = @_;
 
 496   my $dbh   = $self->dbconnect();
 
 498   my (@where, @values);
 
 499   if ($params{login}) {
 
 500     push @where,  'u.login = ?';
 
 501     push @values, $params{login};
 
 504     push @where,  'u.id = ?';
 
 505     push @values, $params{id};
 
 507   my $where = join ' AND ', '1 = 1', @where;
 
 508   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
 
 509                  FROM auth.user_config cfg
 
 510                  LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
 
 512   my $sth   = prepare_execute_query($main::form, $dbh, $query, @values);
 
 516   while (my $ref = $sth->fetchrow_hashref()) {
 
 517     $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
 
 518     @user_data{qw(id login)}    = @{$ref}{qw(id login)};
 
 521   # The XUL/XML & 'CSS new' backed menus have been removed.
 
 522   my %menustyle_map = ( xml => 'new', v4 => 'v3' );
 
 523   $user_data{menustyle} = $menustyle_map{lc($user_data{menustyle} || '')} || $user_data{menustyle};
 
 525   # The 'Win2000.css' stylesheet has been removed.
 
 526   $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
 
 528   # Set default language if selected language does not exist (anymore).
 
 529   $user_data{countrycode} = $::lx_office_conf{system}->{language} unless $user_data{countrycode} && -d "locale/$user_data{countrycode}";
 
 540   my $dbh   = $self->dbconnect();
 
 541   my ($id)  = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
 
 550   my $dbh   = $self->dbconnect;
 
 551   my $id    = $self->get_user_id($login);
 
 560   do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
 
 561   do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
 
 562   do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
 
 564   # TODO: SL::Auth::delete_user
 
 565   # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
 
 570 # --------------------------------------
 
 574 sub restore_session {
 
 577   $session_id        =  $::request->{cgi}->cookie($self->get_session_cookie_name());
 
 578   $session_id        =~ s|[^0-9a-f]||g if $session_id;
 
 580   $self->{SESSION}   = { };
 
 583     return $self->session_restore_result(SESSION_NONE());
 
 586   my ($dbh, $query, $sth, $cookie, $ref, $form);
 
 590   # Don't fail if the auth DB doesn't exist yet.
 
 591   if (!( $dbh = $self->dbconnect(1) )) {
 
 592     return $self->session_restore_result(SESSION_NONE());
 
 595   # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
 
 596   # admin is creating the session tables at the moment.
 
 597   $query  = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
 
 599   if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
 
 600     $sth->finish if $sth;
 
 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   $self->{api_token}   = $cookie->{api_token} if $cookie;
 
 612   my $api_token_cookie = $self->get_api_token_cookie;
 
 613   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
 
 614   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
 
 615   if ($cookie_is_bad) {
 
 616     $self->destroy_session();
 
 617     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
 
 620   if ($self->{column_information}->has('auto_restore')) {
 
 621     $self->_load_with_auto_restore_column($dbh, $session_id);
 
 623     $self->_load_without_auto_restore_column($dbh, $session_id);
 
 626   return $self->session_restore_result(SESSION_OK());
 
 629 sub session_restore_result {
 
 632     $self->{session_restore_result} = $_[0];
 
 634   return $self->{session_restore_result};
 
 637 sub _load_without_auto_restore_column {
 
 638   my ($self, $dbh, $session_id) = @_;
 
 641     SELECT sess_key, sess_value
 
 642     FROM auth.session_content
 
 643     WHERE (session_id = ?)
 
 645   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
 
 647   while (my $ref = $sth->fetchrow_hashref) {
 
 648     my $value = SL::Auth::SessionValue->new(auth  => $self,
 
 649                                             key   => $ref->{sess_key},
 
 650                                             value => $ref->{sess_value},
 
 652     $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 654     next if defined $::form->{$ref->{sess_key}};
 
 656     my $data                    = $value->get;
 
 657     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
 
 661 sub _load_with_auto_restore_column {
 
 662   my ($self, $dbh, $session_id) = @_;
 
 664   my %auto_restore_keys = map { $_ => 1 } qw(login password rpw client_id), SESSION_KEY_ROOT_AUTH, SESSION_KEY_USER_AUTH;
 
 667     SELECT sess_key, sess_value, auto_restore
 
 668     FROM auth.session_content
 
 669     WHERE (session_id = ?) AND (auto_restore OR sess_key IN (@{[ join ',', ("?") x keys %auto_restore_keys ]}))
 
 671   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id, keys %auto_restore_keys);
 
 674   while (my $ref = $sth->fetchrow_hashref) {
 
 675     $need_delete = 1 if $ref->{auto_restore};
 
 676     my $value = SL::Auth::SessionValue->new(auth         => $self,
 
 677                                             key          => $ref->{sess_key},
 
 678                                             value        => $ref->{sess_value},
 
 679                                             auto_restore => $ref->{auto_restore},
 
 681     $self->{SESSION}->{ $ref->{sess_key} } = $value;
 
 683     next if defined $::form->{$ref->{sess_key}};
 
 685     my $data                    = $value->get;
 
 686     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
 
 692     do_query($::form, $dbh, 'DELETE FROM auth.session_content WHERE auto_restore AND session_id = ?', $session_id);
 
 696 sub destroy_session {
 
 700     my $dbh = $self->dbconnect();
 
 704     do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
 
 705     do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
 
 709     SL::SessionFile->destroy_session($session_id);
 
 712     $self->{SESSION} = { };
 
 716 sub active_session_ids {
 
 718   my $dbh   = $self->dbconnect;
 
 720   my $query = qq|SELECT id FROM auth.session|;
 
 722   my @ids   = selectall_array_query($::form, $dbh, $query);
 
 727 sub expire_sessions {
 
 730   return if !$self->session_tables_present;
 
 732   my $dbh   = $self->dbconnect();
 
 734   my $query = qq|SELECT id
 
 736                  WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
 
 738   my @ids   = selectall_array_query($::form, $dbh, $query);
 
 743     SL::SessionFile->destroy_session($_) for @ids;
 
 745     $query = qq|DELETE FROM auth.session_content
 
 746                 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
 
 747     do_query($main::form, $dbh, $query, @ids);
 
 749     $query = qq|DELETE FROM auth.session
 
 750                 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
 
 751     do_query($main::form, $dbh, $query, @ids);
 
 757 sub _create_session_id {
 
 759   map { push @data, int(rand() * 255); } (1..32);
 
 761   my $id = md5_hex(pack 'C*', @data);
 
 766 sub create_or_refresh_session {
 
 767   $session_id ||= shift->_create_session_id;
 
 772   my $provided_dbh = shift;
 
 774   my $dbh          = $provided_dbh || $self->dbconnect(1);
 
 776   return unless $dbh && $session_id;
 
 778   $dbh->begin_work unless $provided_dbh;
 
 780   # If this fails then the "auth" schema might not exist yet, e.g. if
 
 781   # the admin is just trying to create the auth database.
 
 782   if (!$dbh->do(qq|LOCK auth.session_content|)) {
 
 783     $dbh->rollback unless $provided_dbh;
 
 787   my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
 
 790     do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
 
 792     do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
 
 795   if ($self->{column_information}->has('api_token', 'session')) {
 
 796     my ($stored_api_token) = $dbh->selectrow_array(qq|SELECT api_token FROM auth.session WHERE id = ?|, undef, $session_id);
 
 797     do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
 
 800   my @values_to_save = grep    { $_->{modified} }
 
 801                        values %{ $self->{SESSION} };
 
 802   if (@values_to_save) {
 
 803     my %known_keys = map { $_ => 1 }
 
 804       selectall_ids($::form, $dbh, qq|SELECT sess_key FROM auth.session_content WHERE session_id = ?|, 'sess_key', $session_id);
 
 805     my $auto_restore             = $self->{column_information}->has('auto_restore');
 
 807     my $insert_query  = $auto_restore
 
 808       ? "INSERT INTO auth.session_content (session_id, sess_key, sess_value, auto_restore) VALUES (?, ?, ?, ?)"
 
 809       : "INSERT INTO auth.session_content (session_id, sess_key, sess_value) VALUES (?, ?, ?)";
 
 810     my $insert_sth = prepare_query($::form, $dbh, $insert_query);
 
 812     my $update_query  = $auto_restore
 
 813       ? "UPDATE auth.session_content SET sess_value = ?, auto_restore = ? WHERE session_id = ? AND sess_key = ?"
 
 814       : "UPDATE auth.session_content SET sess_value = ? WHERE session_id = ? AND sess_key = ?";
 
 815     my $update_sth = prepare_query($::form, $dbh, $update_query);
 
 817     foreach my $value (@values_to_save) {
 
 818       my @values = ($value->{key}, $value->get_dumped);
 
 819       push @values, $value->{auto_restore} if $auto_restore;
 
 821       if ($known_keys{$value->{key}}) {
 
 822         do_statement($::form, $update_sth, $update_query,
 
 823           $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore, $session_id, $value->{key}
 
 826         do_statement($::form, $insert_sth, $insert_query,
 
 827           $session_id, $value->{key}, $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore
 
 836   $dbh->commit() unless $provided_dbh;
 
 839 sub set_session_value {
 
 843   $self->{SESSION} ||= { };
 
 846     my $key = shift @params;
 
 848     if (ref $key eq 'HASH') {
 
 849       $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key          => $key->{key},
 
 850                                                                       value        => $key->{value},
 
 852                                                                       auto_restore => $key->{auto_restore});
 
 855       my $value = shift @params;
 
 856       $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key   => $key,
 
 865 sub delete_session_value {
 
 868   $self->{SESSION} ||= { };
 
 869   delete @{ $self->{SESSION} }{ @_ };
 
 874 sub get_session_value {
 
 875   my ($self, $key) = @_;
 
 877   return if !$self->{SESSION};
 
 879   ($self->{SESSION}{$key} //= SL::Auth::SessionValue->new(auth => $self, key => $key))->get
 
 882 sub create_unique_session_value {
 
 883   my ($self, $value, %params) = @_;
 
 885   $self->{SESSION} ||= { };
 
 887   my @now                   = gettimeofday();
 
 888   my $key                   = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
 
 889   $self->{unique_counter} ||= 0;
 
 893     $self->{unique_counter}++;
 
 894     $hashed_key = md5_hex($key . $self->{unique_counter});
 
 895   } while (exists $self->{SESSION}->{$hashed_key});
 
 897   $self->set_session_value($hashed_key => $value);
 
 902 sub save_form_in_session {
 
 903   my ($self, %params) = @_;
 
 905   my $form        = delete($params{form}) || $::form;
 
 906   my $non_scalars = delete $params{non_scalars};
 
 909   my %skip_keys   = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
 
 911   foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
 
 912     $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
 
 915   return $self->create_unique_session_value($data, %params);
 
 918 sub restore_form_from_session {
 
 919   my ($self, $key, %params) = @_;
 
 921   my $data = $self->get_session_value($key);
 
 922   return $self unless $data;
 
 924   my $form    = delete($params{form}) || $::form;
 
 925   my $clobber = exists $params{clobber} ? $params{clobber} : 1;
 
 927   map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
 
 932 sub set_cookie_environment_variable {
 
 934   $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
 
 937 sub get_session_cookie_name {
 
 938   my ($self, %params) = @_;
 
 940   $params{type}     ||= 'id';
 
 941   my $name            = $self->{cookie_name} || 'lx_office_erp_session_id';
 
 942   $name              .= '_api_token' if $params{type} eq 'api_token';
 
 951 sub get_api_token_cookie {
 
 954   $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
 
 957 sub is_api_token_cookie_valid {
 
 959   my $provided_api_token = $self->get_api_token_cookie;
 
 960   return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
 
 963 sub _tables_present {
 
 964   my ($self, @tables) = @_;
 
 965   my $cache_key = join '_', @tables;
 
 967   # Only re-check for the presence of auth tables if either the check
 
 968   # hasn't been done before of if they weren't present.
 
 969   return $self->{"$cache_key\_tables_present"} ||= do {
 
 970     my $dbh  = $self->dbconnect(1);
 
 979          WHERE (schemaname = 'auth')
 
 980            AND (tablename IN (@{[ join ', ', ('?') x @tables ]}))|;
 
 982     my ($count) = selectrow_query($main::form, $dbh, $query, @tables);
 
 984     scalar @tables == $count;
 
 988 sub session_tables_present {
 
 989   $_[0]->_tables_present('session', 'session_content');
 
 992 sub master_rights_present {
 
 993   $_[0]->_tables_present('master_rights');
 
 996 # --------------------------------------
 
 998 sub all_rights_full {
 
1001   @{ $self->{master_rights} ||= do {
 
1002       $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY position");
 
1008   return map { $_->[0] } grep { !$_->[2] } $_[0]->all_rights_full;
 
1014   my $form   = $main::form;
 
1016   my $dbh    = $self->dbconnect();
 
1018   my $query  = 'SELECT * FROM auth."group"';
 
1019   my $sth    = prepare_execute_query($form, $dbh, $query);
 
1023   while ($row = $sth->fetchrow_hashref()) {
 
1024     $groups->{$row->{id}} = $row;
 
1028   $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
 
1029   $sth   = prepare_query($form, $dbh, $query);
 
1031   foreach $group (values %{$groups}) {
 
1034     do_statement($form, $sth, $query, $group->{id});
 
1036     while ($row = $sth->fetchrow_hashref()) {
 
1037       push @members, $row->{user_id};
 
1039     $group->{members} = [ uniq @members ];
 
1043   $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
 
1044   $sth   = prepare_query($form, $dbh, $query);
 
1046   foreach $group (values %{$groups}) {
 
1047     $group->{rights} = {};
 
1049     do_statement($form, $sth, $query, $group->{id});
 
1051     while ($row = $sth->fetchrow_hashref()) {
 
1052       $group->{rights}->{$row->{right}} |= $row->{granted};
 
1055     map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } $self->all_rights;
 
1066   my $form  = $main::form;
 
1067   my $dbh   = $self->dbconnect();
 
1071   my ($query, $sth, $row, $rights);
 
1073   if (!$group->{id}) {
 
1074     ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
 
1076     $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
 
1077     do_query($form, $dbh, $query, $group->{id});
 
1080   do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
 
1082   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
 
1084   $query  = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
 
1085   $sth    = prepare_query($form, $dbh, $query);
 
1087   foreach my $user_id (uniq @{ $group->{members} }) {
 
1088     do_statement($form, $sth, $query, $user_id, $group->{id});
 
1092   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
 
1094   $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
 
1095   $sth   = prepare_query($form, $dbh, $query);
 
1097   foreach my $right (keys %{ $group->{rights} }) {
 
1098     do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
 
1109   my $form = $main::form;
 
1111   my $dbh  = $self->dbconnect();
 
1114   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
 
1115   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
 
1116   do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
 
1121 sub evaluate_rights_ary {
 
1128   foreach my $el (@{$ary}) {
 
1129     next unless defined $el;
 
1131     if (ref $el eq "ARRAY") {
 
1132       my $val = evaluate_rights_ary($el);
 
1133       $val    = !$val if $negate;
 
1135       if ($action eq '|') {
 
1141     } elsif (($el eq '&') || ($el eq '|')) {
 
1144     } elsif ($el eq '!') {
 
1147     } elsif ($action eq '|') {
 
1149       $val    = !$val if $negate;
 
1155       $val    = !$val if $negate;
 
1165 sub _parse_rights_string {
 
1174   push @stack, $cur_ary;
 
1176   while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
 
1178     substr($access, 0, length $1) = "";
 
1180     next if ($token =~ /\s/);
 
1182     if ($token eq "(") {
 
1183       my $new_cur_ary = [];
 
1184       push @stack, $new_cur_ary;
 
1185       push @{$cur_ary}, $new_cur_ary;
 
1186       $cur_ary = $new_cur_ary;
 
1188     } elsif ($token eq ")") {
 
1195       $cur_ary = $stack[-1];
 
1197     } elsif (($token eq "|") || ($token eq "&")) {
 
1198       push @{$cur_ary}, $token;
 
1201       push @{$cur_ary}, ($self->{RIGHTS}->{$login}->{$token} // 0) * 1;
 
1205   my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
 
1214   my $default = shift;
 
1216   $self->{FULL_RIGHTS}           ||= { };
 
1217   $self->{FULL_RIGHTS}->{$login} ||= { };
 
1219   if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
 
1220     $self->{RIGHTS}           ||= { };
 
1221     $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
 
1223     $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
 
1226   my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
 
1227   $granted    = $default if (!defined $granted);
 
1235   $::dispatcher->reply_with_json_error(error => 'access') if $::request->type eq 'json';
 
1237   delete $::form->{title};
 
1238   $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
 
1242   my ($self, $right, $dont_abort) = @_;
 
1244   if ($self->check_right($::myconfig{login}, $right)) {
 
1255 sub load_rights_for_user {
 
1256   my ($self, $login) = @_;
 
1257   my $dbh   = $self->dbconnect;
 
1258   my ($query, $sth, $row, $rights);
 
1260   $rights = { map { $_ => 0 } $self->all_rights };
 
1262   return $rights if !$self->client || !$login;
 
1265     qq|SELECT gr."right", gr.granted
 
1266        FROM auth.group_rights gr
 
1269           FROM auth.user_group ug
 
1270           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
 
1274           FROM auth.clients_groups cg
 
1275           WHERE cg.client_id = ?)|;
 
1277   $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
 
1279   while ($row = $sth->fetchrow_hashref()) {
 
1280     $rights->{$row->{right}} |= $row->{granted};
 
1296 SL::Auth - Authentication and session handling
 
1302 =item C<set_session_value @values>
 
1304 =item C<set_session_value %values>
 
1306 Store all values of C<@values> or C<%values> in the session. Each
 
1307 member of C<@values> is tested if it is a hash reference. If it is
 
1308 then it must contain the keys C<key> and C<value> and can optionally
 
1309 contain the key C<auto_restore>. In this case C<value> is associated
 
1310 with C<key> and restored to C<$::form> upon the next request
 
1311 automatically if C<auto_restore> is trueish or if C<value> is a scalar
 
1314 If the current member of C<@values> is not a hash reference then it
 
1315 will be used as the C<key> and the next entry of C<@values> is used as
 
1316 the C<value> to store. In this case setting C<auto_restore> is not
 
1319 Therefore the following two invocations are identical:
 
1321   $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
 
1322   $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
 
1324 All of these values are copied back into C<$::form> for the next
 
1325 request automatically if they're scalar values or if they have
 
1326 C<auto_restore> set to trueish.
 
1328 The values can be any Perl structure. They are stored as YAML dumps.
 
1330 =item C<get_session_value $key>
 
1332 Retrieve a value from the session. Returns C<undef> if the value
 
1335 =item C<create_unique_session_value $value, %params>
 
1337 Create a unique key in the session and store C<$value>
 
1340 Returns the key created in the session.
 
1342 =item C<save_session>
 
1344 Stores the session values in the database. This is the only function
 
1345 that actually stores stuff in the database. Neither the various
 
1346 setters nor the deleter access the database.
 
1348 =item C<save_form_in_session %params>
 
1350 Stores the content of C<$params{form}> (default: C<$::form>) in the
 
1351 session using L</create_unique_session_value>.
 
1353 If C<$params{non_scalars}> is trueish then non-scalar values will be
 
1354 stored as well. Default is to only store scalar values.
 
1356 The following keys will never be saved: C<login>, C<password>,
 
1357 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
 
1358 can be given as an array ref in C<$params{skip_keys}>.
 
1360 Returns the unique key under which the form is stored.
 
1362 =item C<restore_form_from_session $key, %params>
 
1364 Restores the form from the session into C<$params{form}> (default:
 
1367 If C<$params{clobber}> is falsish then existing values with the same
 
1368 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
 
1375 C<reset> deletes every state information from previous requests, but does not
 
1376 close the database connection.
 
1378 Creating a new database handle on each request can take up to 30% of the
 
1379 pre-request startup time, so we want to avoid that for fast ajax calls.
 
1381 =item C<assert, $right, $dont_abort>
 
1383 Checks if current user has the C<$right>. If C<$dont_abort> is falsish
 
1384 the request dies with a access denied error, otherwise returns true or false.
 
1394 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>