374a9f2ffb68b09df218052c293ec9b045d570cc
[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 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
27 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
28
29 use Rose::Object::MakeMethods::Generic (
30   scalar => [ qw(client) ],
31 );
32
33
34 sub new {
35   my ($type, %params) = @_;
36   my $self            = bless {}, $type;
37
38   $self->_read_auth_config(%params);
39   $self->reset;
40
41   return $self;
42 }
43
44 sub reset {
45   my ($self, %params) = @_;
46
47   delete $self->{dbh};
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;
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     ["--crm",                          $locale->text("CRM optional software")],
935     ["crm_search",                     $locale->text("CRM search")],
936     ["crm_new",                        $locale->text("CRM create customers, vendors and contacts")],
937     ["crm_service",                    $locale->text("CRM services")],
938     ["crm_admin",                      $locale->text("CRM admin")],
939     ["crm_adminuser",                  $locale->text("CRM user")],
940     ["crm_adminstatus",                $locale->text("CRM status")],
941     ["crm_email",                      $locale->text("CRM send email")],
942     ["crm_termin",                     $locale->text("CRM termin")],
943     ["crm_opportunity",                $locale->text("CRM opportunity")],
944     ["crm_knowhow",                    $locale->text("CRM know how")],
945     ["crm_follow",                     $locale->text("CRM follow up")],
946     ["crm_notices",                    $locale->text("CRM notices")],
947     ["crm_other",                      $locale->text("CRM other")],
948     ["--master_data",                  $locale->text("Master Data")],
949     ["customer_vendor_edit",           $locale->text("Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)")],
950     ["customer_vendor_all_edit",       $locale->text("Create customers and vendors. Edit all vendors. Edit all customers")],
951     ["part_service_assembly_edit",     $locale->text("Create and edit parts, services, assemblies")],
952     ["part_service_assembly_details",  $locale->text("Show details and reports of parts, services, assemblies")],
953     ["project_edit",                   $locale->text("Create and edit projects")],
954     ["--ar",                           $locale->text("AR")],
955     ["requirement_spec_edit",          $locale->text("Create and edit requirement specs")],
956     ["sales_quotation_edit",           $locale->text("Create and edit sales quotations")],
957     ["sales_order_edit",               $locale->text("Create and edit sales orders")],
958     ["sales_delivery_order_edit",      $locale->text("Create and edit sales delivery orders")],
959     ["invoice_edit",                   $locale->text("Create and edit invoices and credit notes")],
960     ["dunning_edit",                   $locale->text("Create and edit dunnings")],
961     ["sales_all_edit",                 $locale->text("View/edit all employees sales documents")],
962     ["edit_prices",                    $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
963     ["show_ar_transactions",           $locale->text("Show AR transactions as part of AR invoice report")],
964     ["delivery_plan",                  $locale->text("Show delivery plan")],
965     ["delivery_value_report",          $locale->text("Show delivery value report")],
966     ["--ap",                           $locale->text("AP")],
967     ["request_quotation_edit",         $locale->text("Create and edit RFQs")],
968     ["purchase_order_edit",            $locale->text("Create and edit purchase orders")],
969     ["purchase_delivery_order_edit",   $locale->text("Create and edit purchase delivery orders")],
970     ["vendor_invoice_edit",            $locale->text("Create and edit vendor invoices")],
971     ["show_ap_transactions",           $locale->text("Show AP transactions as part of AP invoice report")],
972     ["--warehouse_management",         $locale->text("Warehouse management")],
973     ["warehouse_contents",             $locale->text("View warehouse content")],
974     ["warehouse_management",           $locale->text("Warehouse management")],
975     ["--general_ledger_cash",          $locale->text("General ledger and cash")],
976     ["general_ledger",                 $locale->text("Transactions, AR transactions, AP transactions")],
977     ["datev_export",                   $locale->text("DATEV Export")],
978     ["cash",                           $locale->text("Receipt, payment, reconciliation")],
979     ["--reports",                      $locale->text('Reports')],
980     ["report",                         $locale->text('All reports')],
981     ["advance_turnover_tax_return",    $locale->text('Advance turnover tax return')],
982     ["--batch_printing",               $locale->text("Batch Printing")],
983     ["batch_printing",                 $locale->text("Batch Printing")],
984     ["--configuration",                $locale->text("Configuration")],
985     ["config",                         $locale->text("Change kivitendo installation settings (most entries in the 'System' menu)")],
986     ["admin",                          $locale->text("Client administration: configuration, editing templates, task server control, background jobs (remaining entries in the 'System' menu)")],
987     ["--others",                       $locale->text("Others")],
988     ["email_bcc",                      $locale->text("May set the BCC field when sending emails")],
989     ["productivity",                   $locale->text("Productivity")],
990     ["display_admin_link",             $locale->text("Show administration link")],
991     );
992
993   return @all_rights;
994 }
995
996 sub all_rights {
997   return grep !/^--/, map { $_->[0] } all_rights_full();
998 }
999
1000 sub read_groups {
1001   my $self = shift;
1002
1003   my $form   = $main::form;
1004   my $groups = {};
1005   my $dbh    = $self->dbconnect();
1006
1007   my $query  = 'SELECT * FROM auth."group"';
1008   my $sth    = prepare_execute_query($form, $dbh, $query);
1009
1010   my ($row, $group);
1011
1012   while ($row = $sth->fetchrow_hashref()) {
1013     $groups->{$row->{id}} = $row;
1014   }
1015   $sth->finish();
1016
1017   $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
1018   $sth   = prepare_query($form, $dbh, $query);
1019
1020   foreach $group (values %{$groups}) {
1021     my @members;
1022
1023     do_statement($form, $sth, $query, $group->{id});
1024
1025     while ($row = $sth->fetchrow_hashref()) {
1026       push @members, $row->{user_id};
1027     }
1028     $group->{members} = [ uniq @members ];
1029   }
1030   $sth->finish();
1031
1032   $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
1033   $sth   = prepare_query($form, $dbh, $query);
1034
1035   foreach $group (values %{$groups}) {
1036     $group->{rights} = {};
1037
1038     do_statement($form, $sth, $query, $group->{id});
1039
1040     while ($row = $sth->fetchrow_hashref()) {
1041       $group->{rights}->{$row->{right}} |= $row->{granted};
1042     }
1043
1044     map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } all_rights();
1045   }
1046   $sth->finish();
1047
1048   return $groups;
1049 }
1050
1051 sub save_group {
1052   my $self  = shift;
1053   my $group = shift;
1054
1055   my $form  = $main::form;
1056   my $dbh   = $self->dbconnect();
1057
1058   $dbh->begin_work;
1059
1060   my ($query, $sth, $row, $rights);
1061
1062   if (!$group->{id}) {
1063     ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
1064
1065     $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
1066     do_query($form, $dbh, $query, $group->{id});
1067   }
1068
1069   do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
1070
1071   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
1072
1073   $query  = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
1074   $sth    = prepare_query($form, $dbh, $query);
1075
1076   foreach my $user_id (uniq @{ $group->{members} }) {
1077     do_statement($form, $sth, $query, $user_id, $group->{id});
1078   }
1079   $sth->finish();
1080
1081   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
1082
1083   $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
1084   $sth   = prepare_query($form, $dbh, $query);
1085
1086   foreach my $right (keys %{ $group->{rights} }) {
1087     do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
1088   }
1089   $sth->finish();
1090
1091   $dbh->commit();
1092 }
1093
1094 sub delete_group {
1095   my $self = shift;
1096   my $id   = shift;
1097
1098   my $form = $main::form;
1099
1100   my $dbh  = $self->dbconnect();
1101   $dbh->begin_work;
1102
1103   do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
1104   do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
1105   do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
1106
1107   $dbh->commit();
1108 }
1109
1110 sub evaluate_rights_ary {
1111   my $ary    = shift;
1112
1113   my $value  = 0;
1114   my $action = '|';
1115
1116   foreach my $el (@{$ary}) {
1117     if (ref $el eq "ARRAY") {
1118       if ($action eq '|') {
1119         $value |= evaluate_rights_ary($el);
1120       } else {
1121         $value &= evaluate_rights_ary($el);
1122       }
1123
1124     } elsif (($el eq '&') || ($el eq '|')) {
1125       $action = $el;
1126
1127     } elsif ($action eq '|') {
1128       $value |= $el;
1129
1130     } else {
1131       $value &= $el;
1132
1133     }
1134   }
1135
1136   return $value;
1137 }
1138
1139 sub _parse_rights_string {
1140   my $self   = shift;
1141
1142   my $login  = shift;
1143   my $access = shift;
1144
1145   my @stack;
1146   my $cur_ary = [];
1147
1148   push @stack, $cur_ary;
1149
1150   while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
1151     my $token = $1;
1152     substr($access, 0, length $1) = "";
1153
1154     next if ($token =~ /\s/);
1155
1156     if ($token eq "(") {
1157       my $new_cur_ary = [];
1158       push @stack, $new_cur_ary;
1159       push @{$cur_ary}, $new_cur_ary;
1160       $cur_ary = $new_cur_ary;
1161
1162     } elsif ($token eq ")") {
1163       pop @stack;
1164
1165       if (!@stack) {
1166         return 0;
1167       }
1168
1169       $cur_ary = $stack[-1];
1170
1171     } elsif (($token eq "|") || ($token eq "&")) {
1172       push @{$cur_ary}, $token;
1173
1174     } else {
1175       push @{$cur_ary}, $self->{RIGHTS}->{$login}->{$token} * 1;
1176     }
1177   }
1178
1179   my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
1180
1181   return $result;
1182 }
1183
1184 sub check_right {
1185   my $self    = shift;
1186   my $login   = shift;
1187   my $right   = shift;
1188   my $default = shift;
1189
1190   $self->{FULL_RIGHTS}           ||= { };
1191   $self->{FULL_RIGHTS}->{$login} ||= { };
1192
1193   if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
1194     $self->{RIGHTS}           ||= { };
1195     $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
1196
1197     $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
1198   }
1199
1200   my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
1201   $granted    = $default if (!defined $granted);
1202
1203   return $granted;
1204 }
1205
1206 sub assert {
1207   my ($self, $right, $dont_abort) = @_;
1208
1209   if ($self->check_right($::myconfig{login}, $right)) {
1210     return 1;
1211   }
1212
1213   if (!$dont_abort) {
1214     delete $::form->{title};
1215     $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
1216   }
1217
1218   return 0;
1219 }
1220
1221 sub load_rights_for_user {
1222   my ($self, $login) = @_;
1223   my $dbh   = $self->dbconnect;
1224   my ($query, $sth, $row, $rights);
1225
1226   $rights = { map { $_ => 0 } all_rights() };
1227
1228   return $rights if !$self->client || !$login;
1229
1230   $query =
1231     qq|SELECT gr."right", gr.granted
1232        FROM auth.group_rights gr
1233        WHERE group_id IN
1234          (SELECT ug.group_id
1235           FROM auth.user_group ug
1236           LEFT JOIN auth."user" u ON (ug.user_id = u.id)
1237           WHERE u.login = ?)
1238        AND group_id IN
1239          (SELECT cg.group_id
1240           FROM auth.clients_groups cg
1241           WHERE cg.client_id = ?)|;
1242
1243   $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
1244
1245   while ($row = $sth->fetchrow_hashref()) {
1246     $rights->{$row->{right}} |= $row->{granted};
1247   }
1248   $sth->finish();
1249
1250   return $rights;
1251 }
1252
1253 1;
1254 __END__
1255
1256 =pod
1257
1258 =encoding utf8
1259
1260 =head1 NAME
1261
1262 SL::Auth - Authentication and session handling
1263
1264 =head1 FUNCTIONS
1265
1266 =over 4
1267
1268 =item C<set_session_value @values>
1269
1270 =item C<set_session_value %values>
1271
1272 Store all values of C<@values> or C<%values> in the session. Each
1273 member of C<@values> is tested if it is a hash reference. If it is
1274 then it must contain the keys C<key> and C<value> and can optionally
1275 contain the key C<auto_restore>. In this case C<value> is associated
1276 with C<key> and restored to C<$::form> upon the next request
1277 automatically if C<auto_restore> is trueish or if C<value> is a scalar
1278 value.
1279
1280 If the current member of C<@values> is not a hash reference then it
1281 will be used as the C<key> and the next entry of C<@values> is used as
1282 the C<value> to store. In this case setting C<auto_restore> is not
1283 possible.
1284
1285 Therefore the following two invocations are identical:
1286
1287   $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
1288   $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
1289
1290 All of these values are copied back into C<$::form> for the next
1291 request automatically if they're scalar values or if they have
1292 C<auto_restore> set to trueish.
1293
1294 The values can be any Perl structure. They are stored as YAML dumps.
1295
1296 =item C<get_session_value $key>
1297
1298 Retrieve a value from the session. Returns C<undef> if the value
1299 doesn't exist.
1300
1301 =item C<create_unique_sesion_value $value, %params>
1302
1303 Create a unique key in the session and store C<$value>
1304 there.
1305
1306 Returns the key created in the session.
1307
1308 =item C<save_session>
1309
1310 Stores the session values in the database. This is the only function
1311 that actually stores stuff in the database. Neither the various
1312 setters nor the deleter access the database.
1313
1314 =item <save_form_in_session %params>
1315
1316 Stores the content of C<$params{form}> (default: C<$::form>) in the
1317 session using L</create_unique_sesion_value>.
1318
1319 If C<$params{non_scalars}> is trueish then non-scalar values will be
1320 stored as well. Default is to only store scalar values.
1321
1322 The following keys will never be saved: C<login>, C<password>,
1323 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
1324 can be given as an array ref in C<$params{skip_keys}>.
1325
1326 Returns the unique key under which the form is stored.
1327
1328 =item <restore_form_from_session $key, %params>
1329
1330 Restores the form from the session into C<$params{form}> (default:
1331 C<$::form>).
1332
1333 If C<$params{clobber}> is falsish then existing values with the same
1334 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
1335 is on by default.
1336
1337 Returns C<$self>.
1338
1339 =back
1340
1341 =head1 BUGS
1342
1343 Nothing here yet.
1344
1345 =head1 AUTHOR
1346
1347 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
1348
1349 =cut