f6d9e96d79c4e6741ebd4624a81f5cd3d09f82b2
[kivitendo-erp.git] / SL / Auth.pm
1 package SL::Auth;
2
3 use DBI;
4
5 use Digest::MD5 qw(md5_hex);
6 use IO::File;
7 use Time::HiRes qw(gettimeofday);
8 use List::MoreUtils qw(uniq);
9 use YAML;
10 use Regexp::IPv6 qw($IPv6_re);
11
12 use SL::Auth::ColumnInformation;
13 use SL::Auth::Constants qw(:all);
14 use SL::Auth::DB;
15 use SL::Auth::LDAP;
16 use SL::Auth::Password;
17 use SL::Auth::SessionValue;
18
19 use SL::SessionFile;
20 use SL::User;
21 use SL::DBConnect;
22 use SL::DBUpgrade2;
23 use SL::DBUtils;
24
25 use strict;
26
27 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
28 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
29
30 use Rose::Object::MakeMethods::Generic (
31   scalar => [ qw(client) ],
32 );
33
34
35 sub new {
36   my ($type, %params) = @_;
37   my $self            = bless {}, $type;
38
39   $self->_read_auth_config(%params);
40   $self->reset;
41
42   return $self;
43 }
44
45 sub reset {
46   my ($self, %params) = @_;
47
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);
53   $self->{authenticator}->reset;
54
55   $self->client(undef);
56 }
57
58 sub set_client {
59   my ($self, $id_or_name) = @_;
60
61   $self->client(undef);
62
63   return undef unless $id_or_name;
64
65   my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
66   my $dbh    = $self->dbconnect;
67
68   return undef unless $dbh;
69
70   $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
71
72   return $self->client;
73 }
74
75 sub DESTROY {
76   my $self = shift;
77
78   $self->{dbh}->disconnect() if ($self->{dbh});
79 }
80
81 # form isn't loaded yet, so auth needs it's own error.
82 sub mini_error {
83   $::lxdebug->show_backtrace();
84
85   my ($self, @msg) = @_;
86   if ($ENV{HTTP_USER_AGENT}) {
87     print Form->create_http_response(content_type => 'text/html');
88     print "<pre>", join ('<br>', @msg), "</pre>";
89   } else {
90     print STDERR "Error: @msg\n";
91   }
92   ::end_of_request();
93 }
94
95 sub _read_auth_config {
96   my ($self, %params) = @_;
97
98   map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
99
100   # Prevent password leakage to log files when dumping Auth instances.
101   $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
102
103   if ($params{unit_tests_database}) {
104     $self->{DB_config}   = $::lx_office_conf{'testing/database'};
105     $self->{module}      = 'DB';
106
107   } else {
108     $self->{DB_config}   = $::lx_office_conf{'authentication/database'};
109     $self->{LDAP_config} = $::lx_office_conf{'authentication/ldap'};
110   }
111
112   if ($self->{module} eq 'DB') {
113     $self->{authenticator} = SL::Auth::DB->new($self);
114
115   } elsif ($self->{module} eq 'LDAP') {
116     $self->{authenticator} = SL::Auth::LDAP->new($self);
117   }
118
119   if (!$self->{authenticator}) {
120     my $locale = Locale->new('en');
121     $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
122   }
123
124   my $cfg = $self->{DB_config};
125
126   if (!$cfg) {
127     my $locale = Locale->new('en');
128     $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
129   }
130
131   if (!$cfg->{host} || !$cfg->{db} || !$cfg->{user}) {
132     my $locale = Locale->new('en');
133     $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
134   }
135
136   $self->{authenticator}->verify_config();
137
138   $self->{session_timeout} *= 1;
139   $self->{session_timeout}  = 8 * 60 if (!$self->{session_timeout});
140 }
141
142 sub has_access_to_client {
143   my ($self, $login) = @_;
144
145   return 0 if !$self->client || !$self->client->{id};
146
147   my $sql = <<SQL;
148     SELECT cu.client_id
149     FROM auth.clients_users cu
150     LEFT JOIN auth."user" u ON (cu.user_id = u.id)
151     WHERE (u.login      = ?)
152       AND (cu.client_id = ?)
153 SQL
154
155   my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
156   return $has_access;
157 }
158
159 sub authenticate_root {
160   my ($self, $password) = @_;
161
162   my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
163   if (defined $session_root_auth && $session_root_auth == OK) {
164     return OK;
165   }
166
167   if (!defined $password) {
168     return ERR_PASSWORD;
169   }
170
171   $password             = SL::Auth::Password->hash(login => 'root', password => $password);
172   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
173
174   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
175   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
176
177   return $result;
178 }
179
180 sub authenticate {
181   my ($self, $login, $password) = @_;
182
183   if (!$self->client || !$self->has_access_to_client($login)) {
184     return ERR_PASSWORD;
185   }
186
187   my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
188   if (defined $session_auth && $session_auth == OK) {
189     return OK;
190   }
191
192   if (!defined $password) {
193     return ERR_PASSWORD;
194   }
195
196   my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
197   $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
198   return $result;
199 }
200
201 sub punish_wrong_login {
202   my $failed_login_penalty = ($::lx_office_conf{authentication} || {})->{failed_login_penalty};
203   sleep $failed_login_penalty if $failed_login_penalty;
204 }
205
206 sub get_stored_password {
207   my ($self, $login) = @_;
208
209   my $dbh            = $self->dbconnect;
210
211   return undef unless $dbh;
212
213   my $query             = qq|SELECT password FROM auth."user" WHERE login = ?|;
214   my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
215
216   return $stored_password;
217 }
218
219 sub dbconnect {
220   my $self     = shift;
221   my $may_fail = shift;
222
223   if ($self->{dbh}) {
224     return $self->{dbh};
225   }
226
227   my $cfg = $self->{DB_config};
228   my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
229
230   if ($cfg->{port}) {
231     $dsn .= ';port=' . $cfg->{port};
232   }
233
234   $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
235
236   $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
237
238   if (!$may_fail && !$self->{dbh}) {
239     $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
240   }
241
242   return $self->{dbh};
243 }
244
245 sub dbdisconnect {
246   my $self = shift;
247
248   if ($self->{dbh}) {
249     $self->{dbh}->disconnect();
250     delete $self->{dbh};
251   }
252 }
253
254 sub check_tables {
255   my ($self, $dbh)    = @_;
256
257   $dbh   ||= $self->dbconnect();
258   my $query   = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
259
260   my ($count) = $dbh->selectrow_array($query);
261
262   return $count > 0;
263 }
264
265 sub check_database {
266   my $self = shift;
267
268   my $dbh  = $self->dbconnect(1);
269
270   return $dbh ? 1 : 0;
271 }
272
273 sub create_database {
274   my $self   = shift;
275   my %params = @_;
276
277   my $cfg    = $self->{DB_config};
278
279   if (!$params{superuser}) {
280     $params{superuser}          = $cfg->{user};
281     $params{superuser_password} = $cfg->{password};
282   }
283
284   $params{template} ||= 'template0';
285   $params{template}   =~ s|[^a-zA-Z0-9_\-]||g;
286
287   my $dsn = 'dbi:Pg:dbname=template1;host=' . $cfg->{host};
288
289   if ($cfg->{port}) {
290     $dsn .= ';port=' . $cfg->{port};
291   }
292
293   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database DSN: $dsn");
294
295   my $dbh = SL::DBConnect->connect($dsn, $params{superuser}, $params{superuser_password}, { pg_enable_utf8 => 1 });
296
297   if (!$dbh) {
298     $main::form->error($main::locale->text('The connection to the template database failed:') . "\n" . $DBI::errstr);
299   }
300
301   my $query = qq|CREATE DATABASE "$cfg->{db}" OWNER "$cfg->{user}" TEMPLATE "$params{template}" ENCODING 'UNICODE'|;
302
303   $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database query: $query");
304
305   $dbh->do($query);
306
307   if ($dbh->err) {
308     my $error = $dbh->errstr();
309
310     $query                 = qq|SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'|;
311     my ($cluster_encoding) = $dbh->selectrow_array($query);
312
313     if ($cluster_encoding && ($cluster_encoding !~ m/^(?:UTF-?8|UNICODE)$/i)) {
314       $error = $::locale->text('Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.');
315     }
316
317     $dbh->disconnect();
318
319     $main::form->error($main::locale->text('The creation of the authentication database failed:') . "\n" . $error);
320   }
321
322   $dbh->disconnect();
323 }
324
325 sub create_tables {
326   my $self = shift;
327   my $dbh  = $self->dbconnect();
328
329   $dbh->rollback();
330   SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
331 }
332
333 sub save_user {
334   my $self   = shift;
335   my $login  = shift;
336   my %params = @_;
337
338   my $form   = $main::form;
339
340   my $dbh    = $self->dbconnect();
341
342   my ($sth, $query, $user_id);
343
344   $dbh->begin_work;
345
346   $query     = qq|SELECT id FROM auth."user" WHERE login = ?|;
347   ($user_id) = selectrow_query($form, $dbh, $query, $login);
348
349   if (!$user_id) {
350     $query     = qq|SELECT nextval('auth.user_id_seq')|;
351     ($user_id) = selectrow_query($form, $dbh, $query);
352
353     $query     = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
354     do_query($form, $dbh, $query, $user_id, $login);
355   }
356
357   $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
358   do_query($form, $dbh, $query, $user_id);
359
360   $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
361   $sth   = prepare_query($form, $dbh, $query);
362
363   while (my ($cfg_key, $cfg_value) = each %params) {
364     next if ($cfg_key eq 'password');
365
366     do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
367   }
368
369   $dbh->commit();
370 }
371
372 sub can_change_password {
373   my $self = shift;
374
375   return $self->{authenticator}->can_change_password();
376 }
377
378 sub change_password {
379   my ($self, $login, $new_password) = @_;
380
381   my $result = $self->{authenticator}->change_password($login, $new_password);
382
383   return $result;
384 }
385
386 sub read_all_users {
387   my $self  = shift;
388
389   my $dbh   = $self->dbconnect();
390   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value, s.mtime AS last_action
391
392                  FROM auth."user" AS  u
393
394                  LEFT JOIN auth.user_config AS cfg
395                    ON (cfg.user_id = u.id)
396
397                  LEFT JOIN auth.session_content AS sc_login
398                    ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
399
400                  LEFT JOIN auth.session AS s
401                    ON (s.id = sc_login.session_id)
402               |;
403   my $sth   = prepare_execute_query($main::form, $dbh, $query);
404
405   my %users;
406
407   while (my $ref = $sth->fetchrow_hashref()) {
408
409     $users{$ref->{login}}                    ||= {
410                                                 'login' => $ref->{login},
411                                                 'id' => $ref->{id},
412                                                 'last_action' => $ref->{last_action},
413                                              };
414     $users{$ref->{login}}->{$ref->{cfg_key}}   = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
415   }
416
417   $sth->finish();
418
419   return %users;
420 }
421
422 sub read_user {
423   my ($self, %params) = @_;
424
425   my $dbh   = $self->dbconnect();
426
427   my (@where, @values);
428   if ($params{login}) {
429     push @where,  'u.login = ?';
430     push @values, $params{login};
431   }
432   if ($params{id}) {
433     push @where,  'u.id = ?';
434     push @values, $params{id};
435   }
436   my $where = join ' AND ', '1 = 1', @where;
437   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
438                  FROM auth.user_config cfg
439                  LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
440                  WHERE $where|;
441   my $sth   = prepare_execute_query($main::form, $dbh, $query, @values);
442
443   my %user_data;
444
445   while (my $ref = $sth->fetchrow_hashref()) {
446     $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
447     @user_data{qw(id login)}    = @{$ref}{qw(id login)};
448   }
449
450   # The XUL/XML & 'CSS new' backed menus have been removed.
451   my %menustyle_map = ( xml => 'new', v4 => 'v3' );
452   $user_data{menustyle} = $menustyle_map{lc($user_data{menustyle} || '')} || $user_data{menustyle};
453
454   # The 'Win2000.css' stylesheet has been removed.
455   $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
456
457   # Set default language if selected language does not exist (anymore).
458   $user_data{countrycode} = $::lx_office_conf{system}->{language} unless $user_data{countrycode} && -d "locale/$user_data{countrycode}";
459
460   $sth->finish();
461
462   return %user_data;
463 }
464
465 sub get_user_id {
466   my $self  = shift;
467   my $login = shift;
468
469   my $dbh   = $self->dbconnect();
470   my ($id)  = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
471
472   return $id;
473 }
474
475 sub delete_user {
476   my $self  = shift;
477   my $login = shift;
478
479   my $dbh   = $self->dbconnect;
480   my $id    = $self->get_user_id($login);
481
482   if (!$id) {
483     $dbh->rollback;
484     return;
485   }
486
487   $dbh->begin_work;
488
489   do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
490   do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
491   do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
492
493   # TODO: SL::Auth::delete_user
494   # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
495
496   $dbh->commit;
497 }
498
499 # --------------------------------------
500
501 my $session_id;
502
503 sub restore_session {
504   my $self = shift;
505
506   $session_id        =  $::request->{cgi}->cookie($self->get_session_cookie_name());
507   $session_id        =~ s|[^0-9a-f]||g if $session_id;
508
509   $self->{SESSION}   = { };
510
511   if (!$session_id) {
512     return $self->session_restore_result(SESSION_NONE());
513   }
514
515   my ($dbh, $query, $sth, $cookie, $ref, $form);
516
517   $form   = $main::form;
518
519   # Don't fail if the auth DB doesn't yet.
520   if (!( $dbh = $self->dbconnect(1) )) {
521     return $self->session_restore_result(SESSION_NONE());
522   }
523
524   # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
525   # admin is creating the session tables at the moment.
526   $query  = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
527
528   if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
529     $sth->finish if $sth;
530     return $self->session_restore_result(SESSION_NONE());
531   }
532
533   $cookie = $sth->fetchrow_hashref;
534   $sth->finish;
535
536   # The session ID provided is valid in the following cases:
537   #  1. session ID exists in the database
538   #  2. hasn't expired yet
539   #  3. if cookie for the API token is given: the cookie's value equal database column 'auth.session.api_token' for the session ID
540   #  4. if cookie for the API token is NOT given then: the requestee's IP address must match the stored IP address
541   $self->{api_token}   = $cookie->{api_token} if $cookie;
542   my $api_token_cookie = $self->get_api_token_cookie;
543   my $cookie_is_bad    = !$cookie || $cookie->{is_expired};
544   $cookie_is_bad     ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if  $api_token_cookie;
545   $cookie_is_bad     ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR}                       if !$api_token_cookie && $ENV{REMOTE_ADDR} !~ /^$IPv6_re$/;
546   if ($cookie_is_bad) {
547     $self->destroy_session();
548     return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
549   }
550
551   if ($self->{column_information}->has('auto_restore')) {
552     $self->_load_with_auto_restore_column($dbh, $session_id);
553   } else {
554     $self->_load_without_auto_restore_column($dbh, $session_id);
555   }
556
557   return $self->session_restore_result(SESSION_OK());
558 }
559
560 sub session_restore_result {
561   my $self = shift;
562   if (@_) {
563     $self->{session_restore_result} = $_[0];
564   }
565   return $self->{session_restore_result};
566 }
567
568 sub _load_without_auto_restore_column {
569   my ($self, $dbh, $session_id) = @_;
570
571   my $query = <<SQL;
572     SELECT sess_key, sess_value
573     FROM auth.session_content
574     WHERE (session_id = ?)
575 SQL
576   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
577
578   while (my $ref = $sth->fetchrow_hashref) {
579     my $value = SL::Auth::SessionValue->new(auth  => $self,
580                                             key   => $ref->{sess_key},
581                                             value => $ref->{sess_value},
582                                             raw   => 1);
583     $self->{SESSION}->{ $ref->{sess_key} } = $value;
584
585     next if defined $::form->{$ref->{sess_key}};
586
587     my $data                    = $value->get;
588     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
589   }
590 }
591
592 sub _load_with_auto_restore_column {
593   my ($self, $dbh, $session_id) = @_;
594
595   my $auto_restore_keys = join ', ', map { "'${_}'" } qw(login password rpw);
596
597   my $query = <<SQL;
598     SELECT sess_key, sess_value, auto_restore
599     FROM auth.session_content
600     WHERE (session_id = ?)
601       AND (   auto_restore
602            OR sess_key IN (${auto_restore_keys}))
603 SQL
604   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
605
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},
610                                             auto_restore => $ref->{auto_restore},
611                                             raw          => 1);
612     $self->{SESSION}->{ $ref->{sess_key} } = $value;
613
614     next if defined $::form->{$ref->{sess_key}};
615
616     my $data                    = $value->get;
617     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
618   }
619
620   $sth->finish;
621
622   $query = <<SQL;
623     SELECT sess_key
624     FROM auth.session_content
625     WHERE (session_id = ?)
626       AND NOT COALESCE(auto_restore, FALSE)
627       AND (sess_key NOT IN (${auto_restore_keys}))
628 SQL
629   $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
630
631   while (my $ref = $sth->fetchrow_hashref) {
632     my $value = SL::Auth::SessionValue->new(auth => $self,
633                                             key  => $ref->{sess_key});
634     $self->{SESSION}->{ $ref->{sess_key} } = $value;
635   }
636 }
637
638 sub destroy_session {
639   my $self = shift;
640
641   if ($session_id) {
642     my $dbh = $self->dbconnect();
643
644     $dbh->begin_work;
645
646     do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
647     do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
648
649     $dbh->commit();
650
651     SL::SessionFile->destroy_session($session_id);
652
653     $session_id      = undef;
654     $self->{SESSION} = { };
655   }
656 }
657
658 sub active_session_ids {
659   my $self  = shift;
660   my $dbh   = $self->dbconnect;
661
662   my $query = qq|SELECT id FROM auth.session|;
663
664   my @ids   = selectall_array_query($::form, $dbh, $query);
665
666   return @ids;
667 }
668
669 sub expire_sessions {
670   my $self  = shift;
671
672   return if !$self->session_tables_present;
673
674   my $dbh   = $self->dbconnect();
675
676   my $query = qq|SELECT id
677                  FROM auth.session
678                  WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
679
680   my @ids   = selectall_array_query($::form, $dbh, $query);
681
682   if (@ids) {
683     $dbh->begin_work;
684
685     SL::SessionFile->destroy_session($_) for @ids;
686
687     $query = qq|DELETE FROM auth.session_content
688                 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
689     do_query($main::form, $dbh, $query, @ids);
690
691     $query = qq|DELETE FROM auth.session
692                 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
693     do_query($main::form, $dbh, $query, @ids);
694
695     $dbh->commit();
696   }
697 }
698
699 sub _create_session_id {
700   my @data;
701   map { push @data, int(rand() * 255); } (1..32);
702
703   my $id = md5_hex(pack 'C*', @data);
704
705   return $id;
706 }
707
708 sub create_or_refresh_session {
709   $session_id ||= shift->_create_session_id;
710 }
711
712 sub save_session {
713   my $self         = shift;
714   my $provided_dbh = shift;
715
716   my $dbh          = $provided_dbh || $self->dbconnect(1);
717
718   return unless $dbh && $session_id;
719
720   $dbh->begin_work unless $provided_dbh;
721
722   # If this fails then the "auth" schema might not exist yet, e.g. if
723   # the admin is just trying to create the auth database.
724   if (!$dbh->do(qq|LOCK auth.session_content|)) {
725     $dbh->rollback unless $provided_dbh;
726     return;
727   }
728
729   my @unfetched_keys = map     { $_->{key}        }
730                        grep    { ! $_->{fetched}  }
731                        values %{ $self->{SESSION} };
732   # $::lxdebug->dump(0, "unfetched_keys", [ sort @unfetched_keys ]);
733   # $::lxdebug->dump(0, "all keys", [ sort map { $_->{key} } values %{ $self->{SESSION} } ]);
734   my $query          = qq|DELETE FROM auth.session_content WHERE (session_id = ?)|;
735   $query            .= qq| AND (sess_key NOT IN (| . join(', ', ('?') x scalar @unfetched_keys) . qq|))| if @unfetched_keys;
736
737   do_query($::form, $dbh, $query, $session_id, @unfetched_keys);
738
739   my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
740
741   if ($id) {
742     do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
743   } else {
744     do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
745   }
746
747   if ($self->{column_information}->has('api_token', 'session')) {
748     my ($stored_api_token) = $dbh->selectrow_array(qq|SELECT api_token FROM auth.session WHERE id = ?|, undef, $session_id);
749     do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
750   }
751
752   my @values_to_save = grep    { $_->{fetched} }
753                        values %{ $self->{SESSION} };
754   if (@values_to_save) {
755     my ($columns, $placeholders) = ('', '');
756     my $auto_restore             = $self->{column_information}->has('auto_restore');
757
758     if ($auto_restore) {
759       $columns      .= ', auto_restore';
760       $placeholders .= ', ?';
761     }
762
763     $query  = qq|INSERT INTO auth.session_content (session_id, sess_key, sess_value ${columns}) VALUES (?, ?, ? ${placeholders})|;
764     my $sth = prepare_query($::form, $dbh, $query);
765
766     foreach my $value (@values_to_save) {
767       my @values = ($value->{key}, $value->get_dumped);
768       push @values, $value->{auto_restore} if $auto_restore;
769
770       do_statement($::form, $sth, $query, $session_id, @values);
771     }
772
773     $sth->finish();
774   }
775
776   $dbh->commit() unless $provided_dbh;
777 }
778
779 sub set_session_value {
780   my $self   = shift;
781   my @params = @_;
782
783   $self->{SESSION} ||= { };
784
785   while (@params) {
786     my $key = shift @params;
787
788     if (ref $key eq 'HASH') {
789       $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key          => $key->{key},
790                                                                       value        => $key->{value},
791                                                                       auto_restore => $key->{auto_restore});
792
793     } else {
794       my $value = shift @params;
795       $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key   => $key,
796                                                                value => $value);
797     }
798   }
799
800   return $self;
801 }
802
803 sub delete_session_value {
804   my $self = shift;
805
806   $self->{SESSION} ||= { };
807   delete @{ $self->{SESSION} }{ @_ };
808
809   return $self;
810 }
811
812 sub get_session_value {
813   my $self = shift;
814   my $data = $self->{SESSION} && $self->{SESSION}->{ $_[0] } ? $self->{SESSION}->{ $_[0] }->get : undef;
815
816   return $data;
817 }
818
819 sub create_unique_sesion_value {
820   my ($self, $value, %params) = @_;
821
822   $self->{SESSION} ||= { };
823
824   my @now                   = gettimeofday();
825   my $key                   = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
826   $self->{unique_counter} ||= 0;
827
828   my $hashed_key;
829   do {
830     $self->{unique_counter}++;
831     $hashed_key = md5_hex($key . $self->{unique_counter});
832   } while (exists $self->{SESSION}->{$hashed_key});
833
834   $self->set_session_value($hashed_key => $value);
835
836   return $hashed_key;
837 }
838
839 sub save_form_in_session {
840   my ($self, %params) = @_;
841
842   my $form        = delete($params{form}) || $::form;
843   my $non_scalars = delete $params{non_scalars};
844   my $data        = {};
845
846   my %skip_keys   = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
847
848   foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
849     $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
850   }
851
852   return $self->create_unique_sesion_value($data, %params);
853 }
854
855 sub restore_form_from_session {
856   my ($self, $key, %params) = @_;
857
858   my $data = $self->get_session_value($key);
859   return $self unless $data;
860
861   my $form    = delete($params{form}) || $::form;
862   my $clobber = exists $params{clobber} ? $params{clobber} : 1;
863
864   map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
865
866   return $self;
867 }
868
869 sub set_cookie_environment_variable {
870   my $self = shift;
871   $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
872 }
873
874 sub get_session_cookie_name {
875   my ($self, %params) = @_;
876
877   $params{type}     ||= 'id';
878   my $name            = $self->{cookie_name} || 'lx_office_erp_session_id';
879   $name              .= '_api_token' if $params{type} eq 'api_token';
880
881   return $name;
882 }
883
884 sub get_session_id {
885   return $session_id;
886 }
887
888 sub get_api_token_cookie {
889   my ($self) = @_;
890
891   $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
892 }
893
894 sub is_api_token_cookie_valid {
895   my ($self)             = @_;
896   my $provided_api_token = $self->get_api_token_cookie;
897   return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
898 }
899
900 sub session_tables_present {
901   my $self = shift;
902
903   # Only re-check for the presence of auth tables if either the check
904   # hasn't been done before of if they weren't present.
905   if ($self->{session_tables_present}) {
906     return $self->{session_tables_present};
907   }
908
909   my $dbh  = $self->dbconnect(1);
910
911   if (!$dbh) {
912     return 0;
913   }
914
915   my $query =
916     qq|SELECT COUNT(*)
917        FROM pg_tables
918        WHERE (schemaname = 'auth')
919          AND (tablename IN ('session', 'session_content'))|;
920
921   my ($count) = selectrow_query($main::form, $dbh, $query);
922
923   $self->{session_tables_present} = 2 == $count;
924
925   return $self->{session_tables_present};
926 }
927
928 # --------------------------------------
929
930 sub all_rights_full {
931   my $locale = $main::locale;
932
933   my @all_rights = (
934     ["--master_data",                  $locale->text("Master Data")],
935     ["customer_vendor_edit",           $locale->text("Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)")],
936     ["customer_vendor_all_edit",       $locale->text("Create customers and vendors. Edit all vendors. Edit all customers")],
937     ["part_service_assembly_edit",     $locale->text("Create and edit parts, services, assemblies")],
938     ["part_service_assembly_details",  $locale->text("Show details and reports of parts, services, assemblies")],
939     ["project_edit",                   $locale->text("Create and edit projects")],
940     ["--ar",                           $locale->text("AR")],
941     ["requirement_spec_edit",          $locale->text("Create and edit requirement specs")],
942     ["sales_quotation_edit",           $locale->text("Create and edit sales quotations")],
943     ["sales_order_edit",               $locale->text("Create and edit sales orders")],
944     ["sales_delivery_order_edit",      $locale->text("Create and edit sales delivery orders")],
945     ["invoice_edit",                   $locale->text("Create and edit invoices and credit notes")],
946     ["dunning_edit",                   $locale->text("Create and edit dunnings")],
947     ["sales_letter_edit",              $locale->text("Edit sales letters")],
948     ["sales_all_edit",                 $locale->text("View/edit all employees sales documents")],
949     ["edit_prices",                    $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
950     ["show_ar_transactions",           $locale->text("Show AR transactions as part of AR invoice report")],
951     ["delivery_plan",                  $locale->text("Show delivery plan")],
952     ["delivery_value_report",          $locale->text("Show delivery value report")],
953     ["sales_letter_report",            $locale->text("Show sales letters report")],
954     ["--ap",                           $locale->text("AP")],
955     ["request_quotation_edit",         $locale->text("Create and edit RFQs")],
956     ["purchase_order_edit",            $locale->text("Create and edit purchase orders")],
957     ["purchase_delivery_order_edit",   $locale->text("Create and edit purchase delivery orders")],
958     ["vendor_invoice_edit",            $locale->text("Create and edit vendor invoices")],
959     ["show_ap_transactions",           $locale->text("Show AP transactions as part of AP invoice report")],
960     ["--warehouse_management",         $locale->text("Warehouse management")],
961     ["warehouse_contents",             $locale->text("View warehouse content")],
962     ["warehouse_management",           $locale->text("Warehouse management")],
963     ["--general_ledger_cash",          $locale->text("General ledger and cash")],
964     ["general_ledger",                 $locale->text("Transactions, AR transactions, AP transactions")],
965     ["datev_export",                   $locale->text("DATEV Export")],
966     ["cash",                           $locale->text("Receipt, payment, reconciliation")],
967     ["bank_transaction",               $locale->text("Bank transactions")],
968     ["--reports",                      $locale->text('Reports')],
969     ["report",                         $locale->text('All reports')],
970     ["advance_turnover_tax_return",    $locale->text('Advance turnover tax return')],
971     ["--batch_printing",               $locale->text("Batch Printing")],
972     ["batch_printing",                 $locale->text("Batch Printing")],
973     ["--configuration",                $locale->text("Configuration")],
974     ["config",                         $locale->text("Change kivitendo installation settings (most entries in the 'System' menu)")],
975     ["admin",                          $locale->text("Client administration: configuration, editing templates, task server control, background jobs (remaining entries in the 'System' menu)")],
976     ["--others",                       $locale->text("Others")],
977     ["email_bcc",                      $locale->text("May set the BCC field when sending emails")],
978     ["productivity",                   $locale->text("Productivity")],
979     ["display_admin_link",             $locale->text("Show administration link")],
980     );
981
982   return @all_rights;
983 }
984
985 sub all_rights {
986   return grep !/^--/, map { $_->[0] } all_rights_full();
987 }
988
989 sub read_groups {
990   my $self = shift;
991
992   my $form   = $main::form;
993   my $groups = {};
994   my $dbh    = $self->dbconnect();
995
996   my $query  = 'SELECT * FROM auth."group"';
997   my $sth    = prepare_execute_query($form, $dbh, $query);
998
999   my ($row, $group);
1000
1001   while ($row = $sth->fetchrow_hashref()) {
1002     $groups->{$row->{id}} = $row;
1003   }
1004   $sth->finish();
1005
1006   $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
1007   $sth   = prepare_query($form, $dbh, $query);
1008
1009   foreach $group (values %{$groups}) {
1010     my @members;
1011
1012     do_statement($form, $sth, $query, $group->{id});
1013
1014     while ($row = $sth->fetchrow_hashref()) {
1015       push @members, $row->{user_id};
1016     }
1017     $group->{members} = [ uniq @members ];
1018   }
1019   $sth->finish();
1020
1021   $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
1022   $sth   = prepare_query($form, $dbh, $query);
1023
1024   foreach $group (values %{$groups}) {
1025     $group->{rights} = {};
1026
1027     do_statement($form, $sth, $query, $group->{id});
1028
1029     while ($row = $sth->fetchrow_hashref()) {
1030       $group->{rights}->{$row->{right}} |= $row->{granted};
1031     }
1032
1033     map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } all_rights();
1034   }
1035   $sth->finish();
1036
1037   return $groups;
1038 }
1039
1040 sub save_group {
1041   my $self  = shift;
1042   my $group = shift;
1043
1044   my $form  = $main::form;
1045   my $dbh   = $self->dbconnect();
1046
1047   $dbh->begin_work;
1048
1049   my ($query, $sth, $row, $rights);
1050
1051   if (!$group->{id}) {
1052     ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
1053
1054     $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
1055     do_query($form, $dbh, $query, $group->{id});
1056   }
1057
1058   do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
1059
1060   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
1061
1062   $query  = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
1063   $sth    = prepare_query($form, $dbh, $query);
1064
1065   foreach my $user_id (uniq @{ $group->{members} }) {
1066     do_statement($form, $sth, $query, $user_id, $group->{id});
1067   }
1068   $sth->finish();
1069
1070   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
1071
1072   $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
1073   $sth   = prepare_query($form, $dbh, $query);
1074
1075   foreach my $right (keys %{ $group->{rights} }) {
1076     do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
1077   }
1078   $sth->finish();
1079
1080   $dbh->commit();
1081 }
1082
1083 sub delete_group {
1084   my $self = shift;
1085   my $id   = shift;
1086
1087   my $form = $main::form;
1088
1089   my $dbh  = $self->dbconnect();
1090   $dbh->begin_work;
1091
1092   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
1093   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
1094   do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
1095
1096   $dbh->commit();
1097 }
1098
1099 sub evaluate_rights_ary {
1100   my $ary    = shift;
1101
1102   my $value  = 0;
1103   my $action = '|';
1104
1105   foreach my $el (@{$ary}) {
1106     if (ref $el eq "ARRAY") {
1107       if ($action eq '|') {
1108         $value |= evaluate_rights_ary($el);
1109       } else {
1110         $value &= evaluate_rights_ary($el);
1111       }
1112
1113     } elsif (($el eq '&') || ($el eq '|')) {
1114       $action = $el;
1115
1116     } elsif ($action eq '|') {
1117       $value |= $el;
1118
1119     } else {
1120       $value &= $el;
1121
1122     }
1123   }
1124
1125   return $value;
1126 }
1127
1128 sub _parse_rights_string {
1129   my $self   = shift;
1130
1131   my $login  = shift;
1132   my $access = shift;
1133
1134   my @stack;
1135   my $cur_ary = [];
1136
1137   push @stack, $cur_ary;
1138
1139   while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
1140     my $token = $1;
1141     substr($access, 0, length $1) = "";
1142
1143     next if ($token =~ /\s/);
1144
1145     if ($token eq "(") {
1146       my $new_cur_ary = [];
1147       push @stack, $new_cur_ary;
1148       push @{$cur_ary}, $new_cur_ary;
1149       $cur_ary = $new_cur_ary;
1150
1151     } elsif ($token eq ")") {
1152       pop @stack;
1153
1154       if (!@stack) {
1155         return 0;
1156       }
1157
1158       $cur_ary = $stack[-1];
1159
1160     } elsif (($token eq "|") || ($token eq "&")) {
1161       push @{$cur_ary}, $token;
1162
1163     } else {
1164       push @{$cur_ary}, $self->{RIGHTS}->{$login}->{$token} * 1;
1165     }
1166   }
1167
1168   my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
1169
1170   return $result;
1171 }
1172
1173 sub check_right {
1174   my $self    = shift;
1175   my $login   = shift;
1176   my $right   = shift;
1177   my $default = shift;
1178
1179   $self->{FULL_RIGHTS}           ||= { };
1180   $self->{FULL_RIGHTS}->{$login} ||= { };
1181
1182   if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
1183     $self->{RIGHTS}           ||= { };
1184     $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
1185
1186     $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
1187   }
1188
1189   my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
1190   $granted    = $default if (!defined $granted);
1191
1192   return $granted;
1193 }
1194
1195 sub assert {
1196   my ($self, $right, $dont_abort) = @_;
1197
1198   if ($self->check_right($::myconfig{login}, $right)) {
1199     return 1;
1200   }
1201
1202   if (!$dont_abort) {
1203     delete $::form->{title};
1204     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
1205   }
1206
1207   return 0;
1208 }
1209
1210 sub load_rights_for_user {
1211   my ($self, $login) = @_;
1212   my $dbh   = $self->dbconnect;
1213   my ($query, $sth, $row, $rights);
1214
1215   $rights = { map { $_ => 0 } all_rights() };
1216
1217   return $rights if !$self->client || !$login;
1218
1219   $query =
1220     qq|SELECT gr."right", gr.granted
1221        FROM auth.group_rights gr
1222        WHERE group_id IN
1223          (SELECT ug.group_id
1224           FROM auth.user_group ug
1225           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
1226           WHERE u.login = ?)
1227        AND group_id IN
1228          (SELECT cg.group_id
1229           FROM auth.clients_groups cg
1230           WHERE cg.client_id = ?)|;
1231
1232   $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
1233
1234   while ($row = $sth->fetchrow_hashref()) {
1235     $rights->{$row->{right}} |= $row->{granted};
1236   }
1237   $sth->finish();
1238
1239   return $rights;
1240 }
1241
1242 1;
1243 __END__
1244
1245 =pod
1246
1247 =encoding utf8
1248
1249 =head1 NAME
1250
1251 SL::Auth - Authentication and session handling
1252
1253 =head1 METHODS
1254
1255 =over 4
1256
1257 =item C<set_session_value @values>
1258
1259 =item C<set_session_value %values>
1260
1261 Store all values of C<@values> or C<%values> in the session. Each
1262 member of C<@values> is tested if it is a hash reference. If it is
1263 then it must contain the keys C<key> and C<value> and can optionally
1264 contain the key C<auto_restore>. In this case C<value> is associated
1265 with C<key> and restored to C<$::form> upon the next request
1266 automatically if C<auto_restore> is trueish or if C<value> is a scalar
1267 value.
1268
1269 If the current member of C<@values> is not a hash reference then it
1270 will be used as the C<key> and the next entry of C<@values> is used as
1271 the C<value> to store. In this case setting C<auto_restore> is not
1272 possible.
1273
1274 Therefore the following two invocations are identical:
1275
1276   $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
1277   $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
1278
1279 All of these values are copied back into C<$::form> for the next
1280 request automatically if they're scalar values or if they have
1281 C<auto_restore> set to trueish.
1282
1283 The values can be any Perl structure. They are stored as YAML dumps.
1284
1285 =item C<get_session_value $key>
1286
1287 Retrieve a value from the session. Returns C<undef> if the value
1288 doesn't exist.
1289
1290 =item C<create_unique_sesion_value $value, %params>
1291
1292 Create a unique key in the session and store C<$value>
1293 there.
1294
1295 Returns the key created in the session.
1296
1297 =item C<save_session>
1298
1299 Stores the session values in the database. This is the only function
1300 that actually stores stuff in the database. Neither the various
1301 setters nor the deleter access the database.
1302
1303 =item C<save_form_in_session %params>
1304
1305 Stores the content of C<$params{form}> (default: C<$::form>) in the
1306 session using L</create_unique_sesion_value>.
1307
1308 If C<$params{non_scalars}> is trueish then non-scalar values will be
1309 stored as well. Default is to only store scalar values.
1310
1311 The following keys will never be saved: C<login>, C<password>,
1312 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
1313 can be given as an array ref in C<$params{skip_keys}>.
1314
1315 Returns the unique key under which the form is stored.
1316
1317 =item C<restore_form_from_session $key, %params>
1318
1319 Restores the form from the session into C<$params{form}> (default:
1320 C<$::form>).
1321
1322 If C<$params{clobber}> is falsish then existing values with the same
1323 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
1324 is on by default.
1325
1326 Returns C<$self>.
1327
1328 =item C<reset>
1329
1330 C<reset> deletes every state information from previous requests, but does not
1331 close the database connection.
1332
1333 Creating a new database handle on each request can take up to 30% of the
1334 pre-request startup time, so we want to avoid that for fast ajax calls.
1335
1336 =back
1337
1338 =head1 BUGS
1339
1340 Nothing here yet.
1341
1342 =head1 AUTHOR
1343
1344 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
1345
1346 =cut