d00edb812d9a472af34e0edab2525b8d7ba608ea
[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 => $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, $dbh)    = @_;
248
249   $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, %params) = @_;
442
443   my $dbh   = $self->dbconnect();
444
445   my (@where, @values);
446   if ($params{login}) {
447     push @where,  'u.login = ?';
448     push @values, $params{login};
449   }
450   if ($params{id}) {
451     push @where,  'u.id = ?';
452     push @values, $params{id};
453   }
454   my $where = join ' AND ', '1 = 1', @where;
455   my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
456                  FROM auth.user_config cfg
457                  LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
458                  WHERE $where|;
459   my $sth   = prepare_execute_query($main::form, $dbh, $query, @values);
460
461   my %user_data;
462
463   while (my $ref = $sth->fetchrow_hashref()) {
464     $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
465     @user_data{qw(id login)}    = @{$ref}{qw(id login)};
466   }
467
468   # The XUL/XML backed menu has been removed.
469   $user_data{menustyle} = 'v3' if lc($user_data{menustyle} || '') eq 'xml';
470
471   $sth->finish();
472
473   $main::lxdebug->leave_sub();
474
475   return %user_data;
476 }
477
478 sub get_user_id {
479   $main::lxdebug->enter_sub();
480
481   my $self  = shift;
482   my $login = shift;
483
484   my $dbh   = $self->dbconnect();
485   my ($id)  = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
486
487   $main::lxdebug->leave_sub();
488
489   return $id;
490 }
491
492 sub delete_user {
493   $::lxdebug->enter_sub;
494
495   my $self  = shift;
496   my $login = shift;
497
498   my $dbh   = $self->dbconnect;
499   my $id    = $self->get_user_id($login);
500   my $user_db_exists;
501
502   $dbh->rollback and return $::lxdebug->leave_sub if (!$id);
503
504   my $u_dbh = $self->get_user_dbh($login, may_fail => 1);
505   $user_db_exists = $self->check_tables($u_dbh) if $u_dbh;
506
507   $u_dbh->begin_work if $u_dbh && $user_db_exists;
508
509   $dbh->begin_work;
510
511   do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
512   do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
513   do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
514   do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
515
516   $dbh->commit;
517   $u_dbh->commit if $u_dbh && $user_db_exists;
518
519   $::lxdebug->leave_sub;
520 }
521
522 # --------------------------------------
523
524 my $session_id;
525
526 sub restore_session {
527   $main::lxdebug->enter_sub();
528
529   my $self = shift;
530
531   $session_id        =  $::request->{cgi}->cookie($self->get_session_cookie_name());
532   $session_id        =~ s|[^0-9a-f]||g if $session_id;
533
534   $self->{SESSION}   = { };
535
536   if (!$session_id) {
537     $main::lxdebug->leave_sub();
538     return SESSION_NONE;
539   }
540
541   my ($dbh, $query, $sth, $cookie, $ref, $form);
542
543   $form   = $main::form;
544
545   # Don't fail if the auth DB doesn't yet.
546   if (!( $dbh = $self->dbconnect(1) )) {
547     $::lxdebug->leave_sub;
548     return SESSION_NONE;
549   }
550
551   # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
552   # admin is creating the session tables at the moment.
553   $query  = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
554
555   if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
556     $sth->finish if $sth;
557     $::lxdebug->leave_sub;
558     return SESSION_NONE;
559   }
560
561   $cookie = $sth->fetchrow_hashref;
562   $sth->finish;
563
564   if (!$cookie || $cookie->{is_expired} || ($cookie->{ip_address} ne $ENV{REMOTE_ADDR})) {
565     $self->destroy_session();
566     $main::lxdebug->leave_sub();
567     return $cookie ? SESSION_EXPIRED : SESSION_NONE;
568   }
569
570   if ($self->{column_information}->has('auto_restore')) {
571     $self->_load_with_auto_restore_column($dbh, $session_id);
572   } else {
573     $self->_load_without_auto_restore_column($dbh, $session_id);
574   }
575
576   $main::lxdebug->leave_sub();
577
578   return SESSION_OK;
579 }
580
581 sub _load_without_auto_restore_column {
582   my ($self, $dbh, $session_id) = @_;
583
584   my $query = <<SQL;
585     SELECT sess_key, sess_value
586     FROM auth.session_content
587     WHERE (session_id = ?)
588 SQL
589   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
590
591   while (my $ref = $sth->fetchrow_hashref) {
592     my $value = SL::Auth::SessionValue->new(auth  => $self,
593                                             key   => $ref->{sess_key},
594                                             value => $ref->{sess_value},
595                                             raw   => 1);
596     $self->{SESSION}->{ $ref->{sess_key} } = $value;
597
598     next if defined $::form->{$ref->{sess_key}};
599
600     my $data                    = $value->get;
601     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
602   }
603 }
604
605 sub _load_with_auto_restore_column {
606   my ($self, $dbh, $session_id) = @_;
607
608   my $auto_restore_keys = join ', ', map { "'${_}'" } qw(login password rpw);
609
610   my $query = <<SQL;
611     SELECT sess_key, sess_value, auto_restore
612     FROM auth.session_content
613     WHERE (session_id = ?)
614       AND (   auto_restore
615            OR sess_key IN (${auto_restore_keys}))
616 SQL
617   my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
618
619   while (my $ref = $sth->fetchrow_hashref) {
620     my $value = SL::Auth::SessionValue->new(auth         => $self,
621                                             key          => $ref->{sess_key},
622                                             value        => $ref->{sess_value},
623                                             auto_restore => $ref->{auto_restore},
624                                             raw          => 1);
625     $self->{SESSION}->{ $ref->{sess_key} } = $value;
626
627     next if defined $::form->{$ref->{sess_key}};
628
629     my $data                    = $value->get;
630     $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
631   }
632
633   $sth->finish;
634
635   $query = <<SQL;
636     SELECT sess_key
637     FROM auth.session_content
638     WHERE (session_id = ?)
639       AND NOT COALESCE(auto_restore, FALSE)
640       AND (sess_key NOT IN (${auto_restore_keys}))
641 SQL
642   $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
643
644   while (my $ref = $sth->fetchrow_hashref) {
645     my $value = SL::Auth::SessionValue->new(auth => $self,
646                                             key  => $ref->{sess_key});
647     $self->{SESSION}->{ $ref->{sess_key} } = $value;
648   }
649 }
650
651 sub destroy_session {
652   $main::lxdebug->enter_sub();
653
654   my $self = shift;
655
656   if ($session_id) {
657     my $dbh = $self->dbconnect();
658
659     $dbh->begin_work;
660
661     do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
662     do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
663
664     $dbh->commit();
665
666     SL::SessionFile->destroy_session($session_id);
667
668     $session_id      = undef;
669     $self->{SESSION} = { };
670   }
671
672   $main::lxdebug->leave_sub();
673 }
674
675 sub expire_sessions {
676   $main::lxdebug->enter_sub();
677
678   my $self  = shift;
679
680   $main::lxdebug->leave_sub and return if !$self->session_tables_present;
681
682   my $dbh   = $self->dbconnect();
683
684   my $query = qq|SELECT id
685                  FROM auth.session
686                  WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
687
688   my @ids   = selectall_array_query($::form, $dbh, $query);
689
690   if (@ids) {
691     $dbh->begin_work;
692
693     SL::SessionFile->destroy_session($_) for @ids;
694
695     $query = qq|DELETE FROM auth.session_content
696                 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
697     do_query($main::form, $dbh, $query, @ids);
698
699     $query = qq|DELETE FROM auth.session
700                 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
701     do_query($main::form, $dbh, $query, @ids);
702
703     $dbh->commit();
704   }
705
706   $main::lxdebug->leave_sub();
707 }
708
709 sub _create_session_id {
710   $main::lxdebug->enter_sub();
711
712   my @data;
713   map { push @data, int(rand() * 255); } (1..32);
714
715   my $id = md5_hex(pack 'C*', @data);
716
717   $main::lxdebug->leave_sub();
718
719   return $id;
720 }
721
722 sub create_or_refresh_session {
723   $session_id ||= shift->_create_session_id;
724 }
725
726 sub save_session {
727   $::lxdebug->enter_sub;
728   my $self         = shift;
729   my $provided_dbh = shift;
730
731   my $dbh          = $provided_dbh || $self->dbconnect(1);
732
733   $::lxdebug->leave_sub && return unless $dbh && $session_id;
734
735   $dbh->begin_work unless $provided_dbh;
736
737   # If this fails then the "auth" schema might not exist yet, e.g. if
738   # the admin is just trying to create the auth database.
739   if (!$dbh->do(qq|LOCK auth.session_content|)) {
740     $dbh->rollback unless $provided_dbh;
741     $::lxdebug->leave_sub;
742     return;
743   }
744
745   my @unfetched_keys = map     { $_->{key}        }
746                        grep    { ! $_->{fetched}  }
747                        values %{ $self->{SESSION} };
748   # $::lxdebug->dump(0, "unfetched_keys", [ sort @unfetched_keys ]);
749   # $::lxdebug->dump(0, "all keys", [ sort map { $_->{key} } values %{ $self->{SESSION} } ]);
750   my $query          = qq|DELETE FROM auth.session_content WHERE (session_id = ?)|;
751   $query            .= qq| AND (sess_key NOT IN (| . join(', ', ('?') x scalar @unfetched_keys) . qq|))| if @unfetched_keys;
752
753   do_query($::form, $dbh, $query, $session_id, @unfetched_keys);
754
755   my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
756
757   if ($id) {
758     do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
759   } else {
760     do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
761   }
762
763   my @values_to_save = grep    { $_->{fetched} }
764                        values %{ $self->{SESSION} };
765   if (@values_to_save) {
766     my ($columns, $placeholders) = ('', '');
767     my $auto_restore             = $self->{column_information}->has('auto_restore');
768
769     if ($auto_restore) {
770       $columns      .= ', auto_restore';
771       $placeholders .= ', ?';
772     }
773
774     $query  = qq|INSERT INTO auth.session_content (session_id, sess_key, sess_value ${columns}) VALUES (?, ?, ? ${placeholders})|;
775     my $sth = prepare_query($::form, $dbh, $query);
776
777     foreach my $value (@values_to_save) {
778       my @values = ($value->{key}, $value->get_dumped);
779       push @values, $value->{auto_restore} if $auto_restore;
780
781       do_statement($::form, $sth, $query, $session_id, @values);
782     }
783
784     $sth->finish();
785   }
786
787   $dbh->commit() unless $provided_dbh;
788   $::lxdebug->leave_sub;
789 }
790
791 sub set_session_value {
792   $main::lxdebug->enter_sub();
793
794   my $self   = shift;
795   my @params = @_;
796
797   $self->{SESSION} ||= { };
798
799   while (@params) {
800     my $key = shift @params;
801
802     if (ref $key eq 'HASH') {
803       $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key          => $key->{key},
804                                                                       value        => $key->{value},
805                                                                       auto_restore => $key->{auto_restore});
806
807     } else {
808       my $value = shift @params;
809       $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key   => $key,
810                                                                value => $value);
811     }
812   }
813
814   $main::lxdebug->leave_sub();
815
816   return $self;
817 }
818
819 sub delete_session_value {
820   $main::lxdebug->enter_sub();
821
822   my $self = shift;
823
824   $self->{SESSION} ||= { };
825   delete @{ $self->{SESSION} }{ @_ };
826
827   $main::lxdebug->leave_sub();
828
829   return $self;
830 }
831
832 sub get_session_value {
833   $main::lxdebug->enter_sub();
834
835   my $self = shift;
836   my $data = $self->{SESSION} && $self->{SESSION}->{ $_[0] } ? $self->{SESSION}->{ $_[0] }->get : undef;
837
838   $main::lxdebug->leave_sub();
839
840   return $data;
841 }
842
843 sub create_unique_sesion_value {
844   my ($self, $value, %params) = @_;
845
846   $self->{SESSION} ||= { };
847
848   my @now                   = gettimeofday();
849   my $key                   = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
850   $self->{unique_counter} ||= 0;
851
852   my $hashed_key;
853   do {
854     $self->{unique_counter}++;
855     $hashed_key = md5_hex($key . $self->{unique_counter});
856   } while (exists $self->{SESSION}->{$hashed_key});
857
858   $self->set_session_value($hashed_key => $value);
859
860   return $hashed_key;
861 }
862
863 sub save_form_in_session {
864   my ($self, %params) = @_;
865
866   my $form        = delete($params{form}) || $::form;
867   my $non_scalars = delete $params{non_scalars};
868   my $data        = {};
869
870   my %skip_keys   = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
871
872   foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
873     $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
874   }
875
876   return $self->create_unique_sesion_value($data, %params);
877 }
878
879 sub restore_form_from_session {
880   my ($self, $key, %params) = @_;
881
882   my $data = $self->get_session_value($key);
883   return $self unless $data;
884
885   my $form    = delete($params{form}) || $::form;
886   my $clobber = exists $params{clobber} ? $params{clobber} : 1;
887
888   map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
889
890   return $self;
891 }
892
893 sub set_cookie_environment_variable {
894   my $self = shift;
895   $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
896 }
897
898 sub get_session_cookie_name {
899   my $self = shift;
900
901   return $self->{cookie_name} || 'lx_office_erp_session_id';
902 }
903
904 sub get_session_id {
905   return $session_id;
906 }
907
908 sub session_tables_present {
909   $main::lxdebug->enter_sub();
910
911   my $self = shift;
912
913   # Only re-check for the presence of auth tables if either the check
914   # hasn't been done before of if they weren't present.
915   if ($self->{session_tables_present}) {
916     $main::lxdebug->leave_sub();
917     return $self->{session_tables_present};
918   }
919
920   my $dbh  = $self->dbconnect(1);
921
922   if (!$dbh) {
923     $main::lxdebug->leave_sub();
924     return 0;
925   }
926
927   my $query =
928     qq|SELECT COUNT(*)
929        FROM pg_tables
930        WHERE (schemaname = 'auth')
931          AND (tablename IN ('session', 'session_content'))|;
932
933   my ($count) = selectrow_query($main::form, $dbh, $query);
934
935   $self->{session_tables_present} = 2 == $count;
936
937   $main::lxdebug->leave_sub();
938
939   return $self->{session_tables_present};
940 }
941
942 # --------------------------------------
943
944 sub all_rights_full {
945   my $locale = $main::locale;
946
947   my @all_rights = (
948     ["--crm",                          $locale->text("CRM optional software")],
949     ["crm_search",                     $locale->text("CRM search")],
950     ["crm_new",                        $locale->text("CRM create customers, vendors and contacts")],
951     ["crm_service",                    $locale->text("CRM services")],
952     ["crm_admin",                      $locale->text("CRM admin")],
953     ["crm_adminuser",                  $locale->text("CRM user")],
954     ["crm_adminstatus",                $locale->text("CRM status")],
955     ["crm_email",                      $locale->text("CRM send email")],
956     ["crm_termin",                     $locale->text("CRM termin")],
957     ["crm_opportunity",                $locale->text("CRM opportunity")],
958     ["crm_knowhow",                    $locale->text("CRM know how")],
959     ["crm_follow",                     $locale->text("CRM follow up")],
960     ["crm_notices",                    $locale->text("CRM notices")],
961     ["crm_other",                      $locale->text("CRM other")],
962     ["--master_data",                  $locale->text("Master Data")],
963     ["customer_vendor_edit",           $locale->text("Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)")],
964     ["customer_vendor_all_edit",       $locale->text("Create customers and vendors. Edit all vendors. Edit all customers")],
965     ["part_service_assembly_edit",     $locale->text("Create and edit parts, services, assemblies")],
966     ["project_edit",                   $locale->text("Create and edit projects")],
967     ["--ar",                           $locale->text("AR")],
968     ["sales_quotation_edit",           $locale->text("Create and edit sales quotations")],
969     ["sales_order_edit",               $locale->text("Create and edit sales orders")],
970     ["sales_delivery_order_edit",      $locale->text("Create and edit sales delivery orders")],
971     ["invoice_edit",                   $locale->text("Create and edit invoices and credit notes")],
972     ["dunning_edit",                   $locale->text("Create and edit dunnings")],
973     ["sales_all_edit",                 $locale->text("View/edit all employees sales documents")],
974     ["edit_prices",                    $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
975     ["--ap",                           $locale->text("AP")],
976     ["request_quotation_edit",         $locale->text("Create and edit RFQs")],
977     ["purchase_order_edit",            $locale->text("Create and edit purchase orders")],
978     ["purchase_delivery_order_edit",   $locale->text("Create and edit purchase delivery orders")],
979     ["vendor_invoice_edit",            $locale->text("Create and edit vendor invoices")],
980     ["--warehouse_management",         $locale->text("Warehouse management")],
981     ["warehouse_contents",             $locale->text("View warehouse content")],
982     ["warehouse_management",           $locale->text("Warehouse management")],
983     ["--general_ledger_cash",          $locale->text("General ledger and cash")],
984     ["general_ledger",                 $locale->text("Transactions, AR transactions, AP transactions")],
985     ["datev_export",                   $locale->text("DATEV Export")],
986     ["cash",                           $locale->text("Receipt, payment, reconciliation")],
987     ["--reports",                      $locale->text('Reports')],
988     ["report",                         $locale->text('All reports')],
989     ["advance_turnover_tax_return",    $locale->text('Advance turnover tax return')],
990     ["--batch_printing",               $locale->text("Batch Printing")],
991     ["batch_printing",                 $locale->text("Batch Printing")],
992     ["--others",                       $locale->text("Others")],
993     ["email_bcc",                      $locale->text("May set the BCC field when sending emails")],
994     ["config",                         $locale->text("Change Lx-Office installation settings (all menu entries beneath 'System')")],
995     ["admin",                          $locale->text("Administration (Used to access instance administration from user logins)")],
996     );
997
998   return @all_rights;
999 }
1000
1001 sub all_rights {
1002   return grep !/^--/, map { $_->[0] } all_rights_full();
1003 }
1004
1005 sub read_groups {
1006   $main::lxdebug->enter_sub();
1007
1008   my $self = shift;
1009
1010   my $form   = $main::form;
1011   my $groups = {};
1012   my $dbh    = $self->dbconnect();
1013
1014   my $query  = 'SELECT * FROM auth."group"';
1015   my $sth    = prepare_execute_query($form, $dbh, $query);
1016
1017   my ($row, $group);
1018
1019   while ($row = $sth->fetchrow_hashref()) {
1020     $groups->{$row->{id}} = $row;
1021   }
1022   $sth->finish();
1023
1024   $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
1025   $sth   = prepare_query($form, $dbh, $query);
1026
1027   foreach $group (values %{$groups}) {
1028     my @members;
1029
1030     do_statement($form, $sth, $query, $group->{id});
1031
1032     while ($row = $sth->fetchrow_hashref()) {
1033       push @members, $row->{user_id};
1034     }
1035     $group->{members} = [ uniq @members ];
1036   }
1037   $sth->finish();
1038
1039   $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
1040   $sth   = prepare_query($form, $dbh, $query);
1041
1042   foreach $group (values %{$groups}) {
1043     $group->{rights} = {};
1044
1045     do_statement($form, $sth, $query, $group->{id});
1046
1047     while ($row = $sth->fetchrow_hashref()) {
1048       $group->{rights}->{$row->{right}} |= $row->{granted};
1049     }
1050
1051     map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } all_rights();
1052   }
1053   $sth->finish();
1054
1055   $main::lxdebug->leave_sub();
1056
1057   return $groups;
1058 }
1059
1060 sub save_group {
1061   $main::lxdebug->enter_sub();
1062
1063   my $self  = shift;
1064   my $group = shift;
1065
1066   my $form  = $main::form;
1067   my $dbh   = $self->dbconnect();
1068
1069   $dbh->begin_work;
1070
1071   my ($query, $sth, $row, $rights);
1072
1073   if (!$group->{id}) {
1074     ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
1075
1076     $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
1077     do_query($form, $dbh, $query, $group->{id});
1078   }
1079
1080   do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
1081
1082   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
1083
1084   $query  = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
1085   $sth    = prepare_query($form, $dbh, $query);
1086
1087   foreach my $user_id (uniq @{ $group->{members} }) {
1088     do_statement($form, $sth, $query, $user_id, $group->{id});
1089   }
1090   $sth->finish();
1091
1092   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
1093
1094   $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
1095   $sth   = prepare_query($form, $dbh, $query);
1096
1097   foreach my $right (keys %{ $group->{rights} }) {
1098     do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
1099   }
1100   $sth->finish();
1101
1102   $dbh->commit();
1103
1104   $main::lxdebug->leave_sub();
1105 }
1106
1107 sub delete_group {
1108   $main::lxdebug->enter_sub();
1109
1110   my $self = shift;
1111   my $id   = shift;
1112
1113   my $form = $main::form;
1114
1115   my $dbh  = $self->dbconnect();
1116   $dbh->begin_work;
1117
1118   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
1119   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
1120   do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
1121
1122   $dbh->commit();
1123
1124   $main::lxdebug->leave_sub();
1125 }
1126
1127 sub evaluate_rights_ary {
1128   $main::lxdebug->enter_sub(2);
1129
1130   my $ary    = shift;
1131
1132   my $value  = 0;
1133   my $action = '|';
1134
1135   foreach my $el (@{$ary}) {
1136     if (ref $el eq "ARRAY") {
1137       if ($action eq '|') {
1138         $value |= evaluate_rights_ary($el);
1139       } else {
1140         $value &= evaluate_rights_ary($el);
1141       }
1142
1143     } elsif (($el eq '&') || ($el eq '|')) {
1144       $action = $el;
1145
1146     } elsif ($action eq '|') {
1147       $value |= $el;
1148
1149     } else {
1150       $value &= $el;
1151
1152     }
1153   }
1154
1155   $main::lxdebug->leave_sub(2);
1156
1157   return $value;
1158 }
1159
1160 sub _parse_rights_string {
1161   $main::lxdebug->enter_sub(2);
1162
1163   my $self   = shift;
1164
1165   my $login  = shift;
1166   my $access = shift;
1167
1168   my @stack;
1169   my $cur_ary = [];
1170
1171   push @stack, $cur_ary;
1172
1173   while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
1174     my $token = $1;
1175     substr($access, 0, length $1) = "";
1176
1177     next if ($token =~ /\s/);
1178
1179     if ($token eq "(") {
1180       my $new_cur_ary = [];
1181       push @stack, $new_cur_ary;
1182       push @{$cur_ary}, $new_cur_ary;
1183       $cur_ary = $new_cur_ary;
1184
1185     } elsif ($token eq ")") {
1186       pop @stack;
1187
1188       if (!@stack) {
1189         $main::lxdebug->leave_sub(2);
1190         return 0;
1191       }
1192
1193       $cur_ary = $stack[-1];
1194
1195     } elsif (($token eq "|") || ($token eq "&")) {
1196       push @{$cur_ary}, $token;
1197
1198     } else {
1199       push @{$cur_ary}, $self->{RIGHTS}->{$login}->{$token} * 1;
1200     }
1201   }
1202
1203   my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
1204
1205   $main::lxdebug->leave_sub(2);
1206
1207   return $result;
1208 }
1209
1210 sub check_right {
1211   $main::lxdebug->enter_sub(2);
1212
1213   my $self    = shift;
1214   my $login   = shift;
1215   my $right   = shift;
1216   my $default = shift;
1217
1218   $self->{FULL_RIGHTS}           ||= { };
1219   $self->{FULL_RIGHTS}->{$login} ||= { };
1220
1221   if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
1222     $self->{RIGHTS}           ||= { };
1223     $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
1224
1225     $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
1226   }
1227
1228   my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
1229   $granted    = $default if (!defined $granted);
1230
1231   $main::lxdebug->leave_sub(2);
1232
1233   return $granted;
1234 }
1235
1236 sub assert {
1237   $::lxdebug->enter_sub(2);
1238   my ($self, $right, $dont_abort) = @_;
1239
1240   if ($self->check_right($::myconfig{login}, $right)) {
1241     $::lxdebug->leave_sub(2);
1242     return 1;
1243   }
1244
1245   if (!$dont_abort) {
1246     delete $::form->{title};
1247     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
1248   }
1249
1250   $::lxdebug->leave_sub(2);
1251
1252   return 0;
1253 }
1254
1255 sub load_rights_for_user {
1256   $::lxdebug->enter_sub;
1257
1258   my ($self, $login) = @_;
1259   my $dbh   = $self->dbconnect;
1260   my ($query, $sth, $row, $rights);
1261
1262   $rights = { map { $_ => 0 } all_rights() };
1263
1264   $query =
1265     qq|SELECT gr."right", gr.granted
1266        FROM auth.group_rights gr
1267        WHERE group_id IN
1268          (SELECT ug.group_id
1269           FROM auth.user_group ug
1270           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
1271           WHERE u.login = ?)|;
1272
1273   $sth = prepare_execute_query($::form, $dbh, $query, $login);
1274
1275   while ($row = $sth->fetchrow_hashref()) {
1276     $rights->{$row->{right}} |= $row->{granted};
1277   }
1278   $sth->finish();
1279
1280   $::lxdebug->leave_sub;
1281
1282   return $rights;
1283 }
1284
1285 1;
1286 __END__
1287
1288 =pod
1289
1290 =encoding utf8
1291
1292 =head1 NAME
1293
1294 SL::Auth - Authentication and session handling
1295
1296 =head1 FUNCTIONS
1297
1298 =over 4
1299
1300 =item C<set_session_value @values>
1301 =item C<set_session_value %values>
1302
1303 Store all values of C<@values> or C<%values> in the session. Each
1304 member of C<@values> is tested if it is a hash reference. If it is
1305 then it must contain the keys C<key> and C<value> and can optionally
1306 contain the key C<auto_restore>. In this case C<value> is associated
1307 with C<key> and restored to C<$::form> upon the next request
1308 automatically if C<auto_restore> is trueish or if C<value> is a scalar
1309 value.
1310
1311 If the current member of C<@values> is not a hash reference then it
1312 will be used as the C<key> and the next entry of C<@values> is used as
1313 the C<value> to store. In this case setting C<auto_restore> is not
1314 possible.
1315
1316 Therefore the following two invocations are identical:
1317
1318   $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
1319   $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
1320
1321 All of these values are copied back into C<$::form> for the next
1322 request automatically if they're scalar values or if they have
1323 C<auto_restore> set to trueish.
1324
1325 The values can be any Perl structure. They are stored as YAML dumps.
1326
1327 =item C<get_session_value $key>
1328
1329 Retrieve a value from the session. Returns C<undef> if the value
1330 doesn't exist.
1331
1332 =item C<create_unique_sesion_value $value, %params>
1333
1334 Create a unique key in the session and store C<$value>
1335 there.
1336
1337 Returns the key created in the session.
1338
1339 =item C<save_session>
1340
1341 Stores the session values in the database. This is the only function
1342 that actually stores stuff in the database. Neither the various
1343 setters nor the deleter access the database.
1344
1345 =item <save_form_in_session %params>
1346
1347 Stores the content of C<$params{form}> (default: C<$::form>) in the
1348 session using L</create_unique_sesion_value>.
1349
1350 If C<$params{non_scalars}> is trueish then non-scalar values will be
1351 stored as well. Default is to only store scalar values.
1352
1353 The following keys will never be saved: C<login>, C<password>,
1354 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
1355 can be given as an array ref in C<$params{skip_keys}>.
1356
1357 Returns the unique key under which the form is stored.
1358
1359 =item <restore_form_from_session $key, %params>
1360
1361 Restores the form from the session into C<$params{form}> (default:
1362 C<$::form>).
1363
1364 If C<$params{clobber}> is falsish then existing values with the same
1365 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
1366 is on by default.
1367
1368 Returns C<$self>.
1369
1370 =back
1371
1372 =head1 BUGS
1373
1374 Nothing here yet.
1375
1376 =head1 AUTHOR
1377
1378 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
1379
1380 =cut