5 use Digest::MD5 qw(md5_hex);
7 use Time::HiRes qw(gettimeofday);
8 use List::MoreUtils qw(uniq);
10 use Regexp::IPv6 qw($IPv6_re);
12 use SL::Auth::ColumnInformation;
13 use SL::Auth::Constants qw(:all);
16 use SL::Auth::Password;
17 use SL::Auth::SessionValue;
23 use SL::DBUtils qw(do_query do_statement prepare_execute_query prepare_query selectall_array_query selectrow_query selectall_ids);
27 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
28 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
30 use Rose::Object::MakeMethods::Generic (
31 scalar => [ qw(client) ],
36 my ($type, %params) = @_;
37 my $self = bless {}, $type;
39 $self->_read_auth_config(%params);
46 my ($self, %params) = @_;
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);
56 my ($self, %params) = @_;
58 $self->{SESSION} = { };
59 $self->{FULL_RIGHTS} = { };
60 $self->{RIGHTS} = { };
61 $self->{unique_counter} = 0;
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;
72 delete $self->{column_information};
75 $self->{authenticator}->reset;
81 my ($self, $id_or_name) = @_;
85 return undef unless $id_or_name;
87 my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
88 my $dbh = $self->dbconnect;
90 return undef unless $dbh;
92 $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
97 sub get_default_client_id {
100 my $dbh = $self->dbconnect;
104 my $row = $dbh->selectrow_hashref(qq|SELECT id FROM auth.clients WHERE is_default = TRUE LIMIT 1|);
106 return $row->{id} if $row;
112 $self->{dbh}->disconnect() if ($self->{dbh});
115 # form isn't loaded yet, so auth needs it's own error.
117 $::lxdebug->show_backtrace();
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>";
124 print STDERR "Error: @msg\n";
126 $::dispatcher->end_request;
129 sub _read_auth_config {
130 my ($self, %params) = @_;
132 map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
134 # Prevent password leakage to log files when dumping Auth instances.
135 $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
137 if ($params{unit_tests_database}) {
138 $self->{DB_config} = $::lx_office_conf{'testing/database'};
139 $self->{module} = 'DB';
142 $self->{DB_config} = $::lx_office_conf{'authentication/database'};
145 if ($self->{module} eq 'DB') {
146 $self->{authenticator} = SL::Auth::DB->new($self);
148 } elsif ($self->{module} eq 'LDAP') {
149 $self->{authenticator} = SL::Auth::LDAP->new($::lx_office_conf{'authentication/ldap'});
152 if (!$self->{authenticator}) {
153 my $locale = Locale->new('en');
154 $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
157 my $cfg = $self->{DB_config};
160 my $locale = Locale->new('en');
161 $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
164 if (!$cfg->{host} || !$cfg->{db} || !$cfg->{user}) {
165 my $locale = Locale->new('en');
166 $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
169 $self->{authenticator}->verify_config();
171 $self->{session_timeout} *= 1;
172 $self->{session_timeout} = 8 * 60 if (!$self->{session_timeout});
175 sub has_access_to_client {
176 my ($self, $login) = @_;
178 return 0 if !$self->client || !$self->client->{id};
182 FROM auth.clients_users cu
183 LEFT JOIN auth."user" u ON (cu.user_id = u.id)
185 AND (cu.client_id = ?)
188 my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
192 sub authenticate_root {
193 my ($self, $password) = @_;
195 my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
196 if (defined $session_root_auth && $session_root_auth == OK) {
200 if (!defined $password) {
204 my $admin_password = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
205 $password = SL::Auth::Password->hash(login => 'root', password => $password, stored_password => $admin_password);
207 my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
208 $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
214 my ($self, $login, $password) = @_;
216 if (!$self->client || !$self->has_access_to_client($login)) {
220 my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
221 if (defined $session_auth && $session_auth == OK) {
225 if (!defined $password) {
229 my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
230 $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
234 sub punish_wrong_login {
235 my $failed_login_penalty = ($::lx_office_conf{authentication} || {})->{failed_login_penalty};
236 sleep $failed_login_penalty if $failed_login_penalty;
239 sub get_stored_password {
240 my ($self, $login) = @_;
242 my $dbh = $self->dbconnect;
244 return undef unless $dbh;
246 my $query = qq|SELECT password FROM auth."user" WHERE login = ?|;
247 my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
249 return $stored_password;
254 my $may_fail = shift;
260 my $cfg = $self->{DB_config};
261 my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
264 $dsn .= ';port=' . $cfg->{port};
267 $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
269 $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
271 if (!$may_fail && !$self->{dbh}) {
273 $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
283 $self->{dbh}->disconnect();
288 sub is_db_connected {
290 return !!$self->{dbh};
294 my ($self, $dbh) = @_;
296 $dbh ||= $self->dbconnect();
297 my $query = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
299 my ($count) = $dbh->selectrow_array($query);
307 my $dbh = $self->dbconnect(1);
312 sub create_database {
316 my $cfg = $self->{DB_config};
318 if (!$params{superuser}) {
319 $params{superuser} = $cfg->{user};
320 $params{superuser_password} = $cfg->{password};
323 $params{template} ||= 'template0';
324 $params{template} =~ s|[^a-zA-Z0-9_\-]||g;
326 my $dsn = 'dbi:Pg:dbname=template1;host=' . $cfg->{host};
329 $dsn .= ';port=' . $cfg->{port};
332 $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database DSN: $dsn");
334 my $dbh = SL::DBConnect->connect($dsn, $params{superuser}, $params{superuser_password}, { pg_enable_utf8 => 1 });
337 $main::form->error($main::locale->text('The connection to the template database failed:') . "\n" . $DBI::errstr);
340 my $query = qq|CREATE DATABASE "$cfg->{db}" OWNER "$cfg->{user}" TEMPLATE "$params{template}" ENCODING 'UNICODE'|;
342 $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database query: $query");
347 my $error = $dbh->errstr();
349 $query = qq|SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'|;
350 my ($cluster_encoding) = $dbh->selectrow_array($query);
352 if ($cluster_encoding && ($cluster_encoding !~ m/^(?:UTF-?8|UNICODE)$/i)) {
353 $error = $::locale->text('Your PostgreSQL installationen does not use Unicode as its encoding. This is not supported anymore.');
358 $main::form->error($main::locale->text('The creation of the authentication database failed:') . "\n" . $error);
366 my $dbh = $self->dbconnect();
369 SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
377 my $form = $main::form;
379 my $dbh = $self->dbconnect();
381 my ($sth, $query, $user_id);
385 $query = qq|SELECT id FROM auth."user" WHERE login = ?|;
386 ($user_id) = selectrow_query($form, $dbh, $query, $login);
389 $query = qq|SELECT nextval('auth.user_id_seq')|;
390 ($user_id) = selectrow_query($form, $dbh, $query);
392 $query = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
393 do_query($form, $dbh, $query, $user_id, $login);
396 $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
397 do_query($form, $dbh, $query, $user_id);
399 $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
400 $sth = prepare_query($form, $dbh, $query);
402 while (my ($cfg_key, $cfg_value) = each %params) {
403 next if ($cfg_key eq 'password');
405 do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
411 sub can_change_password {
414 return $self->{authenticator}->can_change_password();
417 sub change_password {
418 my ($self, $login, $new_password) = @_;
420 my $result = $self->{authenticator}->change_password($login, $new_password);
428 my $dbh = $self->dbconnect();
429 my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value, s.mtime AS last_action
431 FROM auth."user" AS u
433 LEFT JOIN auth.user_config AS cfg
434 ON (cfg.user_id = u.id)
436 LEFT JOIN auth.session_content AS sc_login
437 ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
439 LEFT JOIN auth.session AS s
440 ON (s.id = sc_login.session_id)
442 my $sth = prepare_execute_query($main::form, $dbh, $query);
446 while (my $ref = $sth->fetchrow_hashref()) {
448 $users{$ref->{login}} ||= {
449 'login' => $ref->{login},
451 'last_action' => $ref->{last_action},
453 $users{$ref->{login}}->{$ref->{cfg_key}} = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
462 my ($self, %params) = @_;
464 my $dbh = $self->dbconnect();
466 my (@where, @values);
467 if ($params{login}) {
468 push @where, 'u.login = ?';
469 push @values, $params{login};
472 push @where, 'u.id = ?';
473 push @values, $params{id};
475 my $where = join ' AND ', '1 = 1', @where;
476 my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
477 FROM auth.user_config cfg
478 LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
480 my $sth = prepare_execute_query($main::form, $dbh, $query, @values);
484 while (my $ref = $sth->fetchrow_hashref()) {
485 $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
486 @user_data{qw(id login)} = @{$ref}{qw(id login)};
489 # The XUL/XML & 'CSS new' backed menus have been removed.
490 my %menustyle_map = ( xml => 'new', v4 => 'v3' );
491 $user_data{menustyle} = $menustyle_map{lc($user_data{menustyle} || '')} || $user_data{menustyle};
493 # The 'Win2000.css' stylesheet has been removed.
494 $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
496 # Set default language if selected language does not exist (anymore).
497 $user_data{countrycode} = $::lx_office_conf{system}->{language} unless $user_data{countrycode} && -d "locale/$user_data{countrycode}";
508 my $dbh = $self->dbconnect();
509 my ($id) = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
518 my $dbh = $self->dbconnect;
519 my $id = $self->get_user_id($login);
528 do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
529 do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
530 do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
532 # TODO: SL::Auth::delete_user
533 # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
538 # --------------------------------------
542 sub restore_session {
545 $session_id = $::request->{cgi}->cookie($self->get_session_cookie_name());
546 $session_id =~ s|[^0-9a-f]||g if $session_id;
548 $self->{SESSION} = { };
551 return $self->session_restore_result(SESSION_NONE());
554 my ($dbh, $query, $sth, $cookie, $ref, $form);
558 # Don't fail if the auth DB doesn't exist yet.
559 if (!( $dbh = $self->dbconnect(1) )) {
560 return $self->session_restore_result(SESSION_NONE());
563 # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
564 # admin is creating the session tables at the moment.
565 $query = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
567 if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
568 $sth->finish if $sth;
569 return $self->session_restore_result(SESSION_NONE());
572 $cookie = $sth->fetchrow_hashref;
575 # The session ID provided is valid in the following cases:
576 # 1. session ID exists in the database
577 # 2. hasn't expired yet
578 # 3. if cookie for the API token is given: the cookie's value equal database column 'auth.session.api_token' for the session ID
579 $self->{api_token} = $cookie->{api_token} if $cookie;
580 my $api_token_cookie = $self->get_api_token_cookie;
581 my $cookie_is_bad = !$cookie || $cookie->{is_expired};
582 $cookie_is_bad ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if $api_token_cookie;
583 if ($cookie_is_bad) {
584 $self->destroy_session();
585 return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
588 if ($self->{column_information}->has('auto_restore')) {
589 $self->_load_with_auto_restore_column($dbh, $session_id);
591 $self->_load_without_auto_restore_column($dbh, $session_id);
594 return $self->session_restore_result(SESSION_OK());
597 sub session_restore_result {
600 $self->{session_restore_result} = $_[0];
602 return $self->{session_restore_result};
605 sub _load_without_auto_restore_column {
606 my ($self, $dbh, $session_id) = @_;
609 SELECT sess_key, sess_value
610 FROM auth.session_content
611 WHERE (session_id = ?)
613 my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
615 while (my $ref = $sth->fetchrow_hashref) {
616 my $value = SL::Auth::SessionValue->new(auth => $self,
617 key => $ref->{sess_key},
618 value => $ref->{sess_value},
620 $self->{SESSION}->{ $ref->{sess_key} } = $value;
622 next if defined $::form->{$ref->{sess_key}};
624 my $data = $value->get;
625 $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
629 sub _load_with_auto_restore_column {
630 my ($self, $dbh, $session_id) = @_;
632 my %auto_restore_keys = map { $_ => 1 } qw(login password rpw client_id), SESSION_KEY_ROOT_AUTH, SESSION_KEY_USER_AUTH;
635 SELECT sess_key, sess_value, auto_restore
636 FROM auth.session_content
637 WHERE (session_id = ?) AND (auto_restore OR sess_key IN (@{[ join ',', ("?") x keys %auto_restore_keys ]}))
639 my $sth = prepare_execute_query($::form, $dbh, $query, $session_id, keys %auto_restore_keys);
642 while (my $ref = $sth->fetchrow_hashref) {
643 $need_delete = 1 if $ref->{auto_restore};
644 my $value = SL::Auth::SessionValue->new(auth => $self,
645 key => $ref->{sess_key},
646 value => $ref->{sess_value},
647 auto_restore => $ref->{auto_restore},
649 $self->{SESSION}->{ $ref->{sess_key} } = $value;
651 next if defined $::form->{$ref->{sess_key}};
653 my $data = $value->get;
654 $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
660 do_query($::form, $dbh, 'DELETE FROM auth.session_content WHERE auto_restore AND session_id = ?', $session_id);
664 sub destroy_session {
668 my $dbh = $self->dbconnect();
672 do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
673 do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
677 SL::SessionFile->destroy_session($session_id);
680 $self->{SESSION} = { };
684 sub active_session_ids {
686 my $dbh = $self->dbconnect;
688 my $query = qq|SELECT id FROM auth.session|;
690 my @ids = selectall_array_query($::form, $dbh, $query);
695 sub expire_sessions {
698 return if !$self->session_tables_present;
700 my $dbh = $self->dbconnect();
702 my $query = qq|SELECT id
704 WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
706 my @ids = selectall_array_query($::form, $dbh, $query);
711 SL::SessionFile->destroy_session($_) for @ids;
713 $query = qq|DELETE FROM auth.session_content
714 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
715 do_query($main::form, $dbh, $query, @ids);
717 $query = qq|DELETE FROM auth.session
718 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
719 do_query($main::form, $dbh, $query, @ids);
725 sub _create_session_id {
727 map { push @data, int(rand() * 255); } (1..32);
729 my $id = md5_hex(pack 'C*', @data);
734 sub create_or_refresh_session {
735 $session_id ||= shift->_create_session_id;
740 my $provided_dbh = shift;
742 my $dbh = $provided_dbh || $self->dbconnect(1);
744 return unless $dbh && $session_id;
746 $dbh->begin_work unless $provided_dbh;
748 # If this fails then the "auth" schema might not exist yet, e.g. if
749 # the admin is just trying to create the auth database.
750 if (!$dbh->do(qq|LOCK auth.session_content|)) {
751 $dbh->rollback unless $provided_dbh;
755 my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
758 do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
760 do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
763 if ($self->{column_information}->has('api_token', 'session')) {
764 my ($stored_api_token) = $dbh->selectrow_array(qq|SELECT api_token FROM auth.session WHERE id = ?|, undef, $session_id);
765 do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
768 my @values_to_save = grep { $_->{modified} }
769 values %{ $self->{SESSION} };
770 if (@values_to_save) {
771 my %known_keys = map { $_ => 1 }
772 selectall_ids($::form, $dbh, qq|SELECT sess_key FROM auth.session_content WHERE session_id = ?|, 'sess_key', $session_id);
773 my $auto_restore = $self->{column_information}->has('auto_restore');
775 my $insert_query = $auto_restore
776 ? "INSERT INTO auth.session_content (session_id, sess_key, sess_value, auto_restore) VALUES (?, ?, ?, ?)"
777 : "INSERT INTO auth.session_content (session_id, sess_key, sess_value) VALUES (?, ?, ?)";
778 my $insert_sth = prepare_query($::form, $dbh, $insert_query);
780 my $update_query = $auto_restore
781 ? "UPDATE auth.session_content SET sess_value = ?, auto_restore = ? WHERE session_id = ? AND sess_key = ?"
782 : "UPDATE auth.session_content SET sess_value = ? WHERE session_id = ? AND sess_key = ?";
783 my $update_sth = prepare_query($::form, $dbh, $update_query);
785 foreach my $value (@values_to_save) {
786 my @values = ($value->{key}, $value->get_dumped);
787 push @values, $value->{auto_restore} if $auto_restore;
789 if ($known_keys{$value->{key}}) {
790 do_statement($::form, $update_sth, $update_query,
791 $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore, $session_id, $value->{key}
794 do_statement($::form, $insert_sth, $insert_query,
795 $session_id, $value->{key}, $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore
804 $dbh->commit() unless $provided_dbh;
807 sub set_session_value {
811 $self->{SESSION} ||= { };
814 my $key = shift @params;
816 if (ref $key eq 'HASH') {
817 $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key => $key->{key},
818 value => $key->{value},
820 auto_restore => $key->{auto_restore});
823 my $value = shift @params;
824 $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key => $key,
833 sub delete_session_value {
836 $self->{SESSION} ||= { };
837 delete @{ $self->{SESSION} }{ @_ };
842 sub get_session_value {
843 my ($self, $key) = @_;
845 return if !$self->{SESSION};
847 ($self->{SESSION}{$key} //= SL::Auth::SessionValue->new(auth => $self, key => $key))->get
850 sub create_unique_sesion_value {
851 my ($self, $value, %params) = @_;
853 $self->{SESSION} ||= { };
855 my @now = gettimeofday();
856 my $key = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
857 $self->{unique_counter} ||= 0;
861 $self->{unique_counter}++;
862 $hashed_key = md5_hex($key . $self->{unique_counter});
863 } while (exists $self->{SESSION}->{$hashed_key});
865 $self->set_session_value($hashed_key => $value);
870 sub save_form_in_session {
871 my ($self, %params) = @_;
873 my $form = delete($params{form}) || $::form;
874 my $non_scalars = delete $params{non_scalars};
877 my %skip_keys = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
879 foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
880 $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
883 return $self->create_unique_sesion_value($data, %params);
886 sub restore_form_from_session {
887 my ($self, $key, %params) = @_;
889 my $data = $self->get_session_value($key);
890 return $self unless $data;
892 my $form = delete($params{form}) || $::form;
893 my $clobber = exists $params{clobber} ? $params{clobber} : 1;
895 map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
900 sub set_cookie_environment_variable {
902 $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
905 sub get_session_cookie_name {
906 my ($self, %params) = @_;
908 $params{type} ||= 'id';
909 my $name = $self->{cookie_name} || 'lx_office_erp_session_id';
910 $name .= '_api_token' if $params{type} eq 'api_token';
919 sub get_api_token_cookie {
922 $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
925 sub is_api_token_cookie_valid {
927 my $provided_api_token = $self->get_api_token_cookie;
928 return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
931 sub _tables_present {
932 my ($self, @tables) = @_;
933 my $cache_key = join '_', @tables;
935 # Only re-check for the presence of auth tables if either the check
936 # hasn't been done before of if they weren't present.
937 return $self->{"$cache_key\_tables_present"} ||= do {
938 my $dbh = $self->dbconnect(1);
947 WHERE (schemaname = 'auth')
948 AND (tablename IN (@{[ join ', ', ('?') x @tables ]}))|;
950 my ($count) = selectrow_query($main::form, $dbh, $query, @tables);
952 scalar @tables == $count;
956 sub session_tables_present {
957 $_[0]->_tables_present('session', 'session_content');
960 sub master_rights_present {
961 $_[0]->_tables_present('master_rights');
964 # --------------------------------------
966 sub all_rights_full {
969 @{ $self->{master_rights} ||= do {
970 $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY position");
976 return map { $_->[0] } grep { !$_->[2] } $_[0]->all_rights_full;
982 my $form = $main::form;
984 my $dbh = $self->dbconnect();
986 my $query = 'SELECT * FROM auth."group"';
987 my $sth = prepare_execute_query($form, $dbh, $query);
991 while ($row = $sth->fetchrow_hashref()) {
992 $groups->{$row->{id}} = $row;
996 $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
997 $sth = prepare_query($form, $dbh, $query);
999 foreach $group (values %{$groups}) {
1002 do_statement($form, $sth, $query, $group->{id});
1004 while ($row = $sth->fetchrow_hashref()) {
1005 push @members, $row->{user_id};
1007 $group->{members} = [ uniq @members ];
1011 $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
1012 $sth = prepare_query($form, $dbh, $query);
1014 foreach $group (values %{$groups}) {
1015 $group->{rights} = {};
1017 do_statement($form, $sth, $query, $group->{id});
1019 while ($row = $sth->fetchrow_hashref()) {
1020 $group->{rights}->{$row->{right}} |= $row->{granted};
1023 map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } $self->all_rights;
1034 my $form = $main::form;
1035 my $dbh = $self->dbconnect();
1039 my ($query, $sth, $row, $rights);
1041 if (!$group->{id}) {
1042 ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
1044 $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
1045 do_query($form, $dbh, $query, $group->{id});
1048 do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
1050 do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
1052 $query = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
1053 $sth = prepare_query($form, $dbh, $query);
1055 foreach my $user_id (uniq @{ $group->{members} }) {
1056 do_statement($form, $sth, $query, $user_id, $group->{id});
1060 do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
1062 $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
1063 $sth = prepare_query($form, $dbh, $query);
1065 foreach my $right (keys %{ $group->{rights} }) {
1066 do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
1077 my $form = $main::form;
1079 my $dbh = $self->dbconnect();
1082 do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
1083 do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
1084 do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
1089 sub evaluate_rights_ary {
1096 foreach my $el (@{$ary}) {
1097 if (ref $el eq "ARRAY") {
1098 my $val = evaluate_rights_ary($el);
1099 $val = !$val if $negate;
1101 if ($action eq '|') {
1107 } elsif (($el eq '&') || ($el eq '|')) {
1110 } elsif ($el eq '!') {
1113 } elsif ($action eq '|') {
1115 $val = !$val if $negate;
1121 $val = !$val if $negate;
1131 sub _parse_rights_string {
1140 push @stack, $cur_ary;
1142 while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
1144 substr($access, 0, length $1) = "";
1146 next if ($token =~ /\s/);
1148 if ($token eq "(") {
1149 my $new_cur_ary = [];
1150 push @stack, $new_cur_ary;
1151 push @{$cur_ary}, $new_cur_ary;
1152 $cur_ary = $new_cur_ary;
1154 } elsif ($token eq ")") {
1161 $cur_ary = $stack[-1];
1163 } elsif (($token eq "|") || ($token eq "&")) {
1164 push @{$cur_ary}, $token;
1167 push @{$cur_ary}, ($self->{RIGHTS}->{$login}->{$token} // 0) * 1;
1171 my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
1180 my $default = shift;
1182 $self->{FULL_RIGHTS} ||= { };
1183 $self->{FULL_RIGHTS}->{$login} ||= { };
1185 if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
1186 $self->{RIGHTS} ||= { };
1187 $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
1189 $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
1192 my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
1193 $granted = $default if (!defined $granted);
1199 my ($self, $right, $dont_abort) = @_;
1201 if ($self->check_right($::myconfig{login}, $right)) {
1206 delete $::form->{title};
1207 $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
1213 sub load_rights_for_user {
1214 my ($self, $login) = @_;
1215 my $dbh = $self->dbconnect;
1216 my ($query, $sth, $row, $rights);
1218 $rights = { map { $_ => 0 } $self->all_rights };
1220 return $rights if !$self->client || !$login;
1223 qq|SELECT gr."right", gr.granted
1224 FROM auth.group_rights gr
1227 FROM auth.user_group ug
1228 LEFT JOIN auth."user" u ON (ug.user_id = u.id)
1232 FROM auth.clients_groups cg
1233 WHERE cg.client_id = ?)|;
1235 $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
1237 while ($row = $sth->fetchrow_hashref()) {
1238 $rights->{$row->{right}} |= $row->{granted};
1254 SL::Auth - Authentication and session handling
1260 =item C<set_session_value @values>
1262 =item C<set_session_value %values>
1264 Store all values of C<@values> or C<%values> in the session. Each
1265 member of C<@values> is tested if it is a hash reference. If it is
1266 then it must contain the keys C<key> and C<value> and can optionally
1267 contain the key C<auto_restore>. In this case C<value> is associated
1268 with C<key> and restored to C<$::form> upon the next request
1269 automatically if C<auto_restore> is trueish or if C<value> is a scalar
1272 If the current member of C<@values> is not a hash reference then it
1273 will be used as the C<key> and the next entry of C<@values> is used as
1274 the C<value> to store. In this case setting C<auto_restore> is not
1277 Therefore the following two invocations are identical:
1279 $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
1280 $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
1282 All of these values are copied back into C<$::form> for the next
1283 request automatically if they're scalar values or if they have
1284 C<auto_restore> set to trueish.
1286 The values can be any Perl structure. They are stored as YAML dumps.
1288 =item C<get_session_value $key>
1290 Retrieve a value from the session. Returns C<undef> if the value
1293 =item C<create_unique_sesion_value $value, %params>
1295 Create a unique key in the session and store C<$value>
1298 Returns the key created in the session.
1300 =item C<save_session>
1302 Stores the session values in the database. This is the only function
1303 that actually stores stuff in the database. Neither the various
1304 setters nor the deleter access the database.
1306 =item C<save_form_in_session %params>
1308 Stores the content of C<$params{form}> (default: C<$::form>) in the
1309 session using L</create_unique_sesion_value>.
1311 If C<$params{non_scalars}> is trueish then non-scalar values will be
1312 stored as well. Default is to only store scalar values.
1314 The following keys will never be saved: C<login>, C<password>,
1315 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
1316 can be given as an array ref in C<$params{skip_keys}>.
1318 Returns the unique key under which the form is stored.
1320 =item C<restore_form_from_session $key, %params>
1322 Restores the form from the session into C<$params{form}> (default:
1325 If C<$params{clobber}> is falsish then existing values with the same
1326 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
1333 C<reset> deletes every state information from previous requests, but does not
1334 close the database connection.
1336 Creating a new database handle on each request can take up to 30% of the
1337 pre-request startup time, so we want to avoid that for fast ajax calls.
1339 =item C<assert, $right, $dont_abort>
1341 Checks if current user has the C<$right>. If C<$dont_abort> is falsish
1342 the request dies with a access denied error, otherwise returns true or false.
1352 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>