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