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