5 use Digest::MD5 qw(md5_hex);
7 use Time::HiRes qw(gettimeofday);
8 use List::MoreUtils qw(any uniq);
10 use Regexp::IPv6 qw($IPv6_re);
12 use SL::Auth::ColumnInformation;
13 use SL::Auth::Constants qw(:all);
15 use SL::Auth::HTTPHeaders;
17 use SL::Auth::Password;
18 use SL::Auth::SessionValue;
24 use SL::DBUtils qw(do_query do_statement prepare_execute_query prepare_query selectall_array_query selectrow_query selectall_ids);
28 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
29 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
31 use Rose::Object::MakeMethods::Generic (
32 scalar => [ qw(client) ],
37 my ($type, %params) = @_;
38 my $self = bless {}, $type;
40 $self->_read_auth_config(%params);
47 my ($self, %params) = @_;
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);
57 my ($self, %params) = @_;
59 $self->{SESSION} = { };
60 $self->{FULL_RIGHTS} = { };
61 $self->{RIGHTS} = { };
62 $self->{unique_counter} = 0;
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;
73 delete $self->{column_information};
76 $_->reset for @{ $self->{authenticators} };
82 my ($self, $id_or_name) = @_;
86 return undef unless $id_or_name;
88 my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
89 my $dbh = $self->dbconnect;
91 return undef unless $dbh;
93 $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
98 sub get_default_client_id {
101 my $dbh = $self->dbconnect;
105 my $row = $dbh->selectrow_hashref(qq|SELECT id FROM auth.clients WHERE is_default = TRUE LIMIT 1|);
107 return $row->{id} if $row;
113 $self->{dbh}->disconnect() if ($self->{dbh});
116 # form isn't loaded yet, so auth needs it's own error.
118 $::lxdebug->show_backtrace();
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>";
128 print STDERR "Error: @msg\n";
130 $::dispatcher->end_request;
133 sub _read_auth_config {
134 my ($self, %params) = @_;
136 map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
138 # Prevent password leakage to log files when dumping Auth instances.
139 $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
141 if ($params{unit_tests_database}) {
142 $self->{DB_config} = $::lx_office_conf{'testing/database'};
143 $self->{module} = 'DB';
146 $self->{DB_config} = $::lx_office_conf{'authentication/database'};
149 $self->{authenticators} = [];
150 $self->{module} ||= 'DB';
151 $self->{module} =~ s{^ +| +$}{}g;
153 foreach my $module (split m{ +}, $self->{module}) {
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};
160 my $locale = Locale->new('en');
161 $self->mini_error($locale->text('Missing configuration section "authentication/#1" in "config/kivitendo.conf".', $config_name));
164 if ($module eq 'DB') {
165 push @{ $self->{authenticators} }, SL::Auth::DB->new($self);
167 } elsif ($module eq 'LDAP') {
168 push @{ $self->{authenticators} }, SL::Auth::LDAP->new($config);
170 } elsif ($module eq 'HTTPHeaders') {
171 push @{ $self->{authenticators} }, SL::Auth::HTTPHeaders->new($config);
174 my $locale = Locale->new('en');
175 $self->mini_error($locale->text('Unknown authenticantion module #1 specified in "config/kivitendo.conf".', $module));
179 my $cfg = $self->{DB_config};
182 my $locale = Locale->new('en');
183 $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
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".'));
191 $_->verify_config for @{ $self->{authenticators} };
193 $self->{session_timeout} *= 1;
194 $self->{session_timeout} = 8 * 60 if (!$self->{session_timeout});
197 sub has_access_to_client {
198 my ($self, $login) = @_;
200 return 0 if !$self->client || !$self->client->{id};
204 FROM auth.clients_users cu
205 LEFT JOIN auth."user" u ON (cu.user_id = u.id)
207 AND (cu.client_id = ?)
210 my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
214 sub authenticate_root {
215 my ($self, $password) = @_;
217 my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
218 if (defined $session_root_auth && $session_root_auth == OK) {
222 if (!defined $password) {
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);
229 my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
230 $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
235 sub set_session_authenticated {
236 my ($self, $login, $result) = @_;
238 $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
242 my ($self, $login, $password) = @_;
244 if (!$self->client || !$self->has_access_to_client($login)) {
248 my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
249 if (defined $session_auth && $session_auth == OK) {
253 if (!defined $password) {
257 my $result = ERR_USER;
259 foreach my $authenticator (@{ $self->{authenticators} }) {
260 $result = $authenticator->authenticate($login, $password);
261 last if $result == OK;
265 $self->set_session_authenticated($login, $result);
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;
275 sub get_stored_password {
276 my ($self, $login) = @_;
278 my $dbh = $self->dbconnect;
280 return undef unless $dbh;
282 my $query = qq|SELECT password FROM auth."user" WHERE login = ?|;
283 my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
285 return $stored_password;
290 my $may_fail = shift;
296 my $cfg = $self->{DB_config};
297 my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
300 $dsn .= ';port=' . $cfg->{port};
303 $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
305 $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
307 if (!$may_fail && !$self->{dbh}) {
309 $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
319 $self->{dbh}->disconnect();
324 sub is_db_connected {
326 return !!$self->{dbh};
330 my ($self, $dbh) = @_;
332 $dbh ||= $self->dbconnect();
333 my $query = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
335 my ($count) = $dbh->selectrow_array($query);
343 my $dbh = $self->dbconnect(1);
348 sub create_database {
352 my $cfg = $self->{DB_config};
354 if (!$params{superuser}) {
355 $params{superuser} = $cfg->{user};
356 $params{superuser_password} = $cfg->{password};
359 $params{template} ||= 'template0';
360 $params{template} =~ s|[^a-zA-Z0-9_\-]||g;
362 my $dsn = 'dbi:Pg:dbname=template1;host=' . $cfg->{host};
365 $dsn .= ';port=' . $cfg->{port};
368 $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database DSN: $dsn");
370 my $dbh = SL::DBConnect->connect($dsn, $params{superuser}, $params{superuser_password}, { pg_enable_utf8 => 1 });
373 $main::form->error($main::locale->text('The connection to the template database failed:') . "\n" . $DBI::errstr);
376 my $query = qq|CREATE DATABASE "$cfg->{db}" OWNER "$cfg->{user}" TEMPLATE "$params{template}" ENCODING 'UNICODE'|;
378 $main::lxdebug->message(LXDebug->DEBUG1(), "Auth::create_database query: $query");
383 my $error = $dbh->errstr();
385 $query = qq|SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = 'template0'|;
386 my ($cluster_encoding) = $dbh->selectrow_array($query);
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.');
394 $main::form->error($main::locale->text('The creation of the authentication database failed:') . "\n" . $error);
402 my $dbh = $self->dbconnect();
405 SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
413 my $form = $main::form;
415 my $dbh = $self->dbconnect();
417 my ($sth, $query, $user_id);
421 $query = qq|SELECT id FROM auth."user" WHERE login = ?|;
422 ($user_id) = selectrow_query($form, $dbh, $query, $login);
425 $query = qq|SELECT nextval('auth.user_id_seq')|;
426 ($user_id) = selectrow_query($form, $dbh, $query);
428 $query = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
429 do_query($form, $dbh, $query, $user_id, $login);
432 $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
433 do_query($form, $dbh, $query, $user_id);
435 $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
436 $sth = prepare_query($form, $dbh, $query);
438 while (my ($cfg_key, $cfg_value) = each %params) {
439 next if ($cfg_key eq 'password');
441 do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
447 sub can_change_password {
450 return any { $_->can_change_password } @{ $self->{authenticators} };
453 sub change_password {
454 my ($self, $login, $new_password) = @_;
456 my $overall_result = OK;
458 foreach my $authenticator (@{ $self->{authenticators} }) {
459 next unless $authenticator->can_change_password;
461 my $result = $authenticator->change_password($login, $new_password);
462 $overall_result = $result if $result != OK;
465 return $overall_result;
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
474 FROM auth."user" AS u
476 LEFT JOIN auth.user_config AS cfg
477 ON (cfg.user_id = u.id)
479 LEFT JOIN auth.session_content AS sc_login
480 ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
482 LEFT JOIN auth.session AS s
483 ON (s.id = sc_login.session_id)
485 my $sth = prepare_execute_query($main::form, $dbh, $query);
489 while (my $ref = $sth->fetchrow_hashref()) {
491 $users{$ref->{login}} ||= {
492 'login' => $ref->{login},
494 'last_action' => $ref->{last_action},
496 $users{$ref->{login}}->{$ref->{cfg_key}} = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
505 my ($self, %params) = @_;
507 my $dbh = $self->dbconnect();
509 my (@where, @values);
510 if ($params{login}) {
511 push @where, 'u.login = ?';
512 push @values, $params{login};
515 push @where, 'u.id = ?';
516 push @values, $params{id};
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)
523 my $sth = prepare_execute_query($main::form, $dbh, $query, @values);
527 # Set defaults for options not present in database
528 $user_data{follow_up_notify_by_email} = 1;
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)};
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};
539 # The 'Win2000.css' stylesheet has been removed.
540 $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
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}";
554 my $dbh = $self->dbconnect();
555 my ($id) = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
564 my $dbh = $self->dbconnect;
565 my $id = $self->get_user_id($login);
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);
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;
584 # --------------------------------------
588 sub restore_session {
591 $session_id = $::request->{cgi}->cookie($self->get_session_cookie_name());
592 $session_id =~ s|[^0-9a-f]||g if $session_id;
594 $self->{SESSION} = { };
597 return $self->session_restore_result(SESSION_NONE());
600 my ($dbh, $query, $sth, $cookie, $ref, $form);
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());
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 = ?|;
613 if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
614 $sth->finish if $sth;
615 return $self->session_restore_result(SESSION_NONE());
618 $cookie = $sth->fetchrow_hashref;
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());
634 if ($self->{column_information}->has('auto_restore')) {
635 $self->_load_with_auto_restore_column($dbh, $session_id);
637 $self->_load_without_auto_restore_column($dbh, $session_id);
640 return $self->session_restore_result(SESSION_OK());
643 sub session_restore_result {
646 $self->{session_restore_result} = $_[0];
648 return $self->{session_restore_result};
651 sub _load_without_auto_restore_column {
652 my ($self, $dbh, $session_id) = @_;
655 SELECT sess_key, sess_value
656 FROM auth.session_content
657 WHERE (session_id = ?)
659 my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
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},
666 $self->{SESSION}->{ $ref->{sess_key} } = $value;
668 next if defined $::form->{$ref->{sess_key}};
670 my $data = $value->get;
671 $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
675 sub _load_with_auto_restore_column {
676 my ($self, $dbh, $session_id) = @_;
678 my %auto_restore_keys = map { $_ => 1 } qw(login password rpw client_id), SESSION_KEY_ROOT_AUTH, SESSION_KEY_USER_AUTH;
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 ]}))
685 my $sth = prepare_execute_query($::form, $dbh, $query, $session_id, keys %auto_restore_keys);
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},
695 $self->{SESSION}->{ $ref->{sess_key} } = $value;
697 next if defined $::form->{$ref->{sess_key}};
699 my $data = $value->get;
700 $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
706 do_query($::form, $dbh, 'DELETE FROM auth.session_content WHERE auto_restore AND session_id = ?', $session_id);
710 sub destroy_session {
714 my $dbh = $self->dbconnect();
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);
723 SL::SessionFile->destroy_session($session_id);
726 $self->{SESSION} = { };
730 sub active_session_ids {
732 my $dbh = $self->dbconnect;
734 my $query = qq|SELECT id FROM auth.session|;
736 my @ids = selectall_array_query($::form, $dbh, $query);
741 sub expire_sessions {
744 return if !$self->session_tables_present;
746 my $dbh = $self->dbconnect();
748 my $query = qq|SELECT id
750 WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
752 my @ids = selectall_array_query($::form, $dbh, $query);
757 SL::SessionFile->destroy_session($_) for @ids;
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);
763 $query = qq|DELETE FROM auth.session
764 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
765 do_query($main::form, $dbh, $query, @ids);
771 sub _create_session_id {
773 map { push @data, int(rand() * 255); } (1..32);
775 my $id = md5_hex(pack 'C*', @data);
780 sub create_or_refresh_session {
781 $session_id ||= shift->_create_session_id;
786 my $provided_dbh = shift;
788 my $dbh = $provided_dbh || $self->dbconnect(1);
790 return unless $dbh && $session_id;
792 $dbh->begin_work unless $provided_dbh;
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;
801 my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
804 do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
806 do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
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;
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');
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);
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);
831 foreach my $value (@values_to_save) {
832 my @values = ($value->{key}, $value->get_dumped);
833 push @values, $value->{auto_restore} if $auto_restore;
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}
840 do_statement($::form, $insert_sth, $insert_query,
841 $session_id, $value->{key}, $value->get_dumped, ( $value->{auto_restore} )x!!$auto_restore
850 $dbh->commit() unless $provided_dbh;
853 sub set_session_value {
857 $self->{SESSION} ||= { };
860 my $key = shift @params;
862 if (ref $key eq 'HASH') {
863 $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key => $key->{key},
864 value => $key->{value},
866 auto_restore => $key->{auto_restore});
869 my $value = shift @params;
870 $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key => $key,
879 sub delete_session_value {
882 $self->{SESSION} ||= { };
883 delete @{ $self->{SESSION} }{ @_ };
888 sub get_session_value {
889 my ($self, $key) = @_;
891 return if !$self->{SESSION};
893 ($self->{SESSION}{$key} //= SL::Auth::SessionValue->new(auth => $self, key => $key))->get
896 sub create_unique_session_value {
897 my ($self, $value, %params) = @_;
899 $self->{SESSION} ||= { };
901 my @now = gettimeofday();
902 my $key = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
903 $self->{unique_counter} ||= 0;
907 $self->{unique_counter}++;
908 $hashed_key = md5_hex($key . $self->{unique_counter});
909 } while (exists $self->{SESSION}->{$hashed_key});
911 $self->set_session_value($hashed_key => $value);
916 sub save_form_in_session {
917 my ($self, %params) = @_;
919 my $form = delete($params{form}) || $::form;
920 my $non_scalars = delete $params{non_scalars};
923 my %skip_keys = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
925 foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
926 $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
929 return $self->create_unique_session_value($data, %params);
932 sub restore_form_from_session {
933 my ($self, $key, %params) = @_;
935 my $data = $self->get_session_value($key);
936 return $self unless $data;
938 my $form = delete($params{form}) || $::form;
939 my $clobber = exists $params{clobber} ? $params{clobber} : 1;
941 map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
946 sub set_cookie_environment_variable {
948 $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
951 sub get_session_cookie_name {
952 my ($self, %params) = @_;
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';
965 sub get_api_token_cookie {
968 $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
971 sub is_api_token_cookie_valid {
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);
977 sub _tables_present {
978 my ($self, @tables) = @_;
979 my $cache_key = join '_', @tables;
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);
993 WHERE (schemaname = 'auth')
994 AND (tablename IN (@{[ join ', ', ('?') x @tables ]}))|;
996 my ($count) = selectrow_query($main::form, $dbh, $query, @tables);
998 scalar @tables == $count;
1002 sub session_tables_present {
1003 $_[0]->_tables_present('session', 'session_content');
1006 sub master_rights_present {
1007 $_[0]->_tables_present('master_rights');
1010 # --------------------------------------
1012 sub all_rights_full {
1015 @{ $self->{master_rights} ||= do {
1016 $self->dbconnect->selectall_arrayref("SELECT name, description, category FROM auth.master_rights ORDER BY position");
1022 return map { $_->[0] } grep { !$_->[2] } $_[0]->all_rights_full;
1028 my $form = $main::form;
1030 my $dbh = $self->dbconnect();
1032 my $query = 'SELECT * FROM auth."group"';
1033 my $sth = prepare_execute_query($form, $dbh, $query);
1037 while ($row = $sth->fetchrow_hashref()) {
1038 $groups->{$row->{id}} = $row;
1042 $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
1043 $sth = prepare_query($form, $dbh, $query);
1045 foreach $group (values %{$groups}) {
1048 do_statement($form, $sth, $query, $group->{id});
1050 while ($row = $sth->fetchrow_hashref()) {
1051 push @members, $row->{user_id};
1053 $group->{members} = [ uniq @members ];
1057 $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
1058 $sth = prepare_query($form, $dbh, $query);
1060 foreach $group (values %{$groups}) {
1061 $group->{rights} = {};
1063 do_statement($form, $sth, $query, $group->{id});
1065 while ($row = $sth->fetchrow_hashref()) {
1066 $group->{rights}->{$row->{right}} |= $row->{granted};
1069 map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } $self->all_rights;
1080 my $form = $main::form;
1081 my $dbh = $self->dbconnect();
1085 my ($query, $sth, $row, $rights);
1087 if (!$group->{id}) {
1088 ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
1090 $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
1091 do_query($form, $dbh, $query, $group->{id});
1094 do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
1096 do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
1098 $query = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
1099 $sth = prepare_query($form, $dbh, $query);
1101 foreach my $user_id (uniq @{ $group->{members} }) {
1102 do_statement($form, $sth, $query, $user_id, $group->{id});
1106 do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
1108 $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
1109 $sth = prepare_query($form, $dbh, $query);
1111 foreach my $right (keys %{ $group->{rights} }) {
1112 do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
1123 my $form = $main::form;
1125 my $dbh = $self->dbconnect();
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);
1135 sub evaluate_rights_ary {
1142 foreach my $el (@{$ary}) {
1143 next unless defined $el;
1145 if (ref $el eq "ARRAY") {
1146 my $val = evaluate_rights_ary($el);
1147 $val = !$val if $negate;
1149 if ($action eq '|') {
1155 } elsif (($el eq '&') || ($el eq '|')) {
1158 } elsif ($el eq '!') {
1161 } elsif ($action eq '|') {
1163 $val = !$val if $negate;
1169 $val = !$val if $negate;
1179 sub _parse_rights_string {
1188 push @stack, $cur_ary;
1190 while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
1192 substr($access, 0, length $1) = "";
1194 next if ($token =~ /\s/);
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;
1202 } elsif ($token eq ")") {
1209 $cur_ary = $stack[-1];
1211 } elsif (($token eq "|") || ($token eq "&")) {
1212 push @{$cur_ary}, $token;
1215 push @{$cur_ary}, ($self->{RIGHTS}->{$login}->{$token} // 0) * 1;
1219 my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
1228 my $default = shift;
1230 $self->{FULL_RIGHTS} ||= { };
1231 $self->{FULL_RIGHTS}->{$login} ||= { };
1233 if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
1234 $self->{RIGHTS} ||= { };
1235 $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
1237 $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
1240 my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
1241 $granted = $default if (!defined $granted);
1249 $::dispatcher->reply_with_json_error(error => 'access') if $::request->type eq 'json';
1251 delete $::form->{title};
1252 $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
1256 my ($self, $right, $dont_abort) = @_;
1258 if ($self->check_right($::myconfig{login}, $right)) {
1269 sub load_rights_for_user {
1270 my ($self, $login) = @_;
1271 my $dbh = $self->dbconnect;
1272 my ($query, $sth, $row, $rights);
1274 $rights = { map { $_ => 0 } $self->all_rights };
1276 return $rights if !$self->client || !$login;
1279 qq|SELECT gr."right", gr.granted
1280 FROM auth.group_rights gr
1283 FROM auth.user_group ug
1284 LEFT JOIN auth."user" u ON (ug.user_id = u.id)
1288 FROM auth.clients_groups cg
1289 WHERE cg.client_id = ?)|;
1291 $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
1293 while ($row = $sth->fetchrow_hashref()) {
1294 $rights->{$row->{right}} |= $row->{granted};
1310 SL::Auth - Authentication and session handling
1316 =item C<set_session_value @values>
1318 =item C<set_session_value %values>
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
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
1333 Therefore the following two invocations are identical:
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" });
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.
1342 The values can be any Perl structure. They are stored as YAML dumps.
1344 =item C<get_session_value $key>
1346 Retrieve a value from the session. Returns C<undef> if the value
1349 =item C<create_unique_session_value $value, %params>
1351 Create a unique key in the session and store C<$value>
1354 Returns the key created in the session.
1356 =item C<save_session>
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.
1362 =item C<save_form_in_session %params>
1364 Stores the content of C<$params{form}> (default: C<$::form>) in the
1365 session using L</create_unique_session_value>.
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.
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}>.
1374 Returns the unique key under which the form is stored.
1376 =item C<restore_form_from_session $key, %params>
1378 Restores the form from the session into C<$params{form}> (default:
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}>
1389 C<reset> deletes every state information from previous requests, but does not
1390 close the database connection.
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.
1395 =item C<assert, $right, $dont_abort>
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.
1408 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>