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