5 use Digest::MD5 qw(md5_hex);
7 use Time::HiRes qw(gettimeofday);
8 use List::MoreUtils qw(uniq);
11 use SL::Auth::ColumnInformation;
12 use SL::Auth::Constants qw(:all);
15 use SL::Auth::Password;
16 use SL::Auth::SessionValue;
26 use constant SESSION_KEY_ROOT_AUTH => 'session_auth_status_root';
27 use constant SESSION_KEY_USER_AUTH => 'session_auth_status_user';
29 use Rose::Object::MakeMethods::Generic (
30 scalar => [ qw(client) ],
35 $main::lxdebug->enter_sub();
37 my ($type, %params) = @_;
38 my $self = bless {}, $type;
40 $self->_read_auth_config(%params);
43 $main::lxdebug->leave_sub();
49 my ($self, %params) = @_;
52 $self->{SESSION} = { };
53 $self->{FULL_RIGHTS} = { };
54 $self->{RIGHTS} = { };
55 $self->{unique_counter} = 0;
56 $self->{column_information} = SL::Auth::ColumnInformation->new(auth => $self);
57 $self->{authenticator}->reset;
63 my ($self, $id_or_name) = @_;
67 return undef unless $id_or_name;
69 my $column = $id_or_name =~ m/^\d+$/ ? 'id' : 'name';
70 my $dbh = $self->dbconnect;
72 return undef unless $dbh;
74 $self->client($dbh->selectrow_hashref(qq|SELECT * FROM auth.clients WHERE ${column} = ?|, undef, $id_or_name));
82 $self->{dbh}->disconnect() if ($self->{dbh});
85 # form isn't loaded yet, so auth needs it's own error.
87 $::lxdebug->show_backtrace();
89 my ($self, @msg) = @_;
90 if ($ENV{HTTP_USER_AGENT}) {
91 print Form->create_http_response(content_type => 'text/html');
92 print "<pre>", join ('<br>', @msg), "</pre>";
94 print STDERR "Error: @msg\n";
99 sub _read_auth_config {
100 $main::lxdebug->enter_sub();
102 my ($self, %params) = @_;
104 map { $self->{$_} = $::lx_office_conf{authentication}->{$_} } keys %{ $::lx_office_conf{authentication} };
106 # Prevent password leakage to log files when dumping Auth instances.
107 $self->{admin_password} = sub { $::lx_office_conf{authentication}->{admin_password} };
109 if ($params{unit_tests_database}) {
110 $self->{DB_config} = $::lx_office_conf{'testing/database'};
111 $self->{module} = 'DB';
114 $self->{DB_config} = $::lx_office_conf{'authentication/database'};
115 $self->{LDAP_config} = $::lx_office_conf{'authentication/ldap'};
118 if ($self->{module} eq 'DB') {
119 $self->{authenticator} = SL::Auth::DB->new($self);
121 } elsif ($self->{module} eq 'LDAP') {
122 $self->{authenticator} = SL::Auth::LDAP->new($self);
125 if (!$self->{authenticator}) {
126 my $locale = Locale->new('en');
127 $self->mini_error($locale->text('No or an unknown authenticantion module specified in "config/kivitendo.conf".'));
130 my $cfg = $self->{DB_config};
133 my $locale = Locale->new('en');
134 $self->mini_error($locale->text('config/kivitendo.conf: Key "DB_config" is missing.'));
137 if (!$cfg->{host} || !$cfg->{db} || !$cfg->{user}) {
138 my $locale = Locale->new('en');
139 $self->mini_error($locale->text('config/kivitendo.conf: Missing parameters in "authentication/database". Required parameters are "host", "db" and "user".'));
142 $self->{authenticator}->verify_config();
144 $self->{session_timeout} *= 1;
145 $self->{session_timeout} = 8 * 60 if (!$self->{session_timeout});
147 $main::lxdebug->leave_sub();
150 sub has_access_to_client {
151 my ($self, $login) = @_;
153 return 0 if !$self->client || !$self->client->{id};
157 FROM auth.clients_users cu
158 LEFT JOIN auth."user" u ON (cu.user_id = u.id)
160 AND (cu.client_id = ?)
163 my ($has_access) = $self->dbconnect->selectrow_array($sql, undef, $login, $self->client->{id});
167 sub authenticate_root {
168 $main::lxdebug->enter_sub();
170 my ($self, $password) = @_;
172 my $session_root_auth = $self->get_session_value(SESSION_KEY_ROOT_AUTH());
173 if (defined $session_root_auth && $session_root_auth == OK) {
174 $::lxdebug->leave_sub;
178 if (!defined $password) {
179 $::lxdebug->leave_sub;
183 $password = SL::Auth::Password->hash(login => 'root', password => $password);
184 my $admin_password = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
186 my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
187 $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
189 $::lxdebug->leave_sub;
194 $main::lxdebug->enter_sub();
196 my ($self, $login, $password) = @_;
198 if (!$self->client || !$self->has_access_to_client($login)) {
199 $::lxdebug->leave_sub;
203 my $session_auth = $self->get_session_value(SESSION_KEY_USER_AUTH());
204 if (defined $session_auth && $session_auth == OK) {
205 $::lxdebug->leave_sub;
209 if (!defined $password) {
210 $::lxdebug->leave_sub;
214 my $result = $login ? $self->{authenticator}->authenticate($login, $password) : ERR_USER;
215 $self->set_session_value(SESSION_KEY_USER_AUTH() => $result, login => $login, client_id => $self->client->{id});
217 $::lxdebug->leave_sub;
221 sub punish_wrong_login {
222 my $failed_login_penalty = ($::lx_office_conf{authentication} || {})->{failed_login_penalty};
223 sleep $failed_login_penalty if $failed_login_penalty;
226 sub get_stored_password {
227 my ($self, $login) = @_;
229 my $dbh = $self->dbconnect;
231 return undef unless $dbh;
233 my $query = qq|SELECT password FROM auth."user" WHERE login = ?|;
234 my ($stored_password) = $dbh->selectrow_array($query, undef, $login);
236 return $stored_password;
240 $main::lxdebug->enter_sub(2);
243 my $may_fail = shift;
246 $main::lxdebug->leave_sub(2);
250 my $cfg = $self->{DB_config};
251 my $dsn = 'dbi:Pg:dbname=' . $cfg->{db} . ';host=' . $cfg->{host};
254 $dsn .= ';port=' . $cfg->{port};
257 $main::lxdebug->message(LXDebug->DEBUG1, "Auth::dbconnect DSN: $dsn");
259 $self->{dbh} = SL::DBConnect->connect($dsn, $cfg->{user}, $cfg->{password}, { pg_enable_utf8 => 1, AutoCommit => 1 });
261 if (!$may_fail && !$self->{dbh}) {
262 $main::form->error($main::locale->text('The connection to the authentication database failed:') . "\n" . $DBI::errstr);
265 $main::lxdebug->leave_sub(2);
271 $main::lxdebug->enter_sub();
276 $self->{dbh}->disconnect();
280 $main::lxdebug->leave_sub();
284 $main::lxdebug->enter_sub();
286 my ($self, $dbh) = @_;
288 $dbh ||= $self->dbconnect();
289 my $query = qq|SELECT COUNT(*) FROM pg_tables WHERE (schemaname = 'auth') AND (tablename = 'user')|;
291 my ($count) = $dbh->selectrow_array($query);
293 $main::lxdebug->leave_sub();
299 $main::lxdebug->enter_sub();
303 my $dbh = $self->dbconnect(1);
305 $main::lxdebug->leave_sub();
310 sub create_database {
311 $main::lxdebug->enter_sub();
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);
363 $main::lxdebug->leave_sub();
367 $main::lxdebug->enter_sub();
370 my $dbh = $self->dbconnect();
373 SL::DBUpgrade2->new(form => $::form)->process_query($dbh, 'sql/auth_db.sql');
375 $main::lxdebug->leave_sub();
379 $main::lxdebug->enter_sub();
385 my $form = $main::form;
387 my $dbh = $self->dbconnect();
389 my ($sth, $query, $user_id);
393 $query = qq|SELECT id FROM auth."user" WHERE login = ?|;
394 ($user_id) = selectrow_query($form, $dbh, $query, $login);
397 $query = qq|SELECT nextval('auth.user_id_seq')|;
398 ($user_id) = selectrow_query($form, $dbh, $query);
400 $query = qq|INSERT INTO auth."user" (id, login) VALUES (?, ?)|;
401 do_query($form, $dbh, $query, $user_id, $login);
404 $query = qq|DELETE FROM auth.user_config WHERE (user_id = ?)|;
405 do_query($form, $dbh, $query, $user_id);
407 $query = qq|INSERT INTO auth.user_config (user_id, cfg_key, cfg_value) VALUES (?, ?, ?)|;
408 $sth = prepare_query($form, $dbh, $query);
410 while (my ($cfg_key, $cfg_value) = each %params) {
411 next if ($cfg_key eq 'password');
413 do_statement($form, $sth, $query, $user_id, $cfg_key, $cfg_value);
418 $main::lxdebug->leave_sub();
421 sub can_change_password {
424 return $self->{authenticator}->can_change_password();
427 sub change_password {
428 $main::lxdebug->enter_sub();
430 my ($self, $login, $new_password) = @_;
432 my $result = $self->{authenticator}->change_password($login, $new_password);
434 $main::lxdebug->leave_sub();
440 $main::lxdebug->enter_sub();
444 my $dbh = $self->dbconnect();
445 my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value, s.mtime AS last_action
447 FROM auth."user" AS u
449 LEFT JOIN auth.user_config AS cfg
450 ON (cfg.user_id = u.id)
452 LEFT JOIN auth.session_content AS sc_login
453 ON (sc_login.sess_key = 'login' AND sc_login.sess_value = ('--- ' \|\| u.login \|\| '\n'))
455 LEFT JOIN auth.session AS s
456 ON (s.id = sc_login.session_id)
458 my $sth = prepare_execute_query($main::form, $dbh, $query);
462 while (my $ref = $sth->fetchrow_hashref()) {
464 $users{$ref->{login}} ||= {
465 'login' => $ref->{login},
467 'last_action' => $ref->{last_action},
469 $users{$ref->{login}}->{$ref->{cfg_key}} = $ref->{cfg_value} if (($ref->{cfg_key} ne 'login') && ($ref->{cfg_key} ne 'id'));
474 $main::lxdebug->leave_sub();
480 $main::lxdebug->enter_sub();
482 my ($self, %params) = @_;
484 my $dbh = $self->dbconnect();
486 my (@where, @values);
487 if ($params{login}) {
488 push @where, 'u.login = ?';
489 push @values, $params{login};
492 push @where, 'u.id = ?';
493 push @values, $params{id};
495 my $where = join ' AND ', '1 = 1', @where;
496 my $query = qq|SELECT u.id, u.login, cfg.cfg_key, cfg.cfg_value
497 FROM auth.user_config cfg
498 LEFT JOIN auth."user" u ON (cfg.user_id = u.id)
500 my $sth = prepare_execute_query($main::form, $dbh, $query, @values);
504 while (my $ref = $sth->fetchrow_hashref()) {
505 $user_data{$ref->{cfg_key}} = $ref->{cfg_value};
506 @user_data{qw(id login)} = @{$ref}{qw(id login)};
509 # The XUL/XML & 'CSS new' backed menus have been removed.
510 my %menustyle_map = ( xml => 'new', v4 => 'v3' );
511 $user_data{menustyle} = $menustyle_map{lc($user_data{menustyle} || '')} || $user_data{menustyle};
513 # The 'Win2000.css' stylesheet has been removed.
514 $user_data{stylesheet} = 'kivitendo.css' if ($user_data{stylesheet} || '') =~ m/win2000/i;
516 # Set default language if selected language does not exist (anymore).
517 $user_data{countrycode} = $::lx_office_conf{system}->{language} unless $user_data{countrycode} && -d "locale/$user_data{countrycode}";
521 $main::lxdebug->leave_sub();
527 $main::lxdebug->enter_sub();
532 my $dbh = $self->dbconnect();
533 my ($id) = selectrow_query($main::form, $dbh, qq|SELECT id FROM auth."user" WHERE login = ?|, $login);
535 $main::lxdebug->leave_sub();
541 $::lxdebug->enter_sub;
546 my $dbh = $self->dbconnect;
547 my $id = $self->get_user_id($login);
549 $dbh->rollback and return $::lxdebug->leave_sub if (!$id);
553 do_query($::form, $dbh, qq|DELETE FROM auth.user_group WHERE user_id = ?|, $id);
554 do_query($::form, $dbh, qq|DELETE FROM auth.user_config WHERE user_id = ?|, $id);
555 do_query($::form, $dbh, qq|DELETE FROM auth.user WHERE id = ?|, $id);
557 # TODO: SL::Auth::delete_user
558 # do_query($::form, $u_dbh, qq|UPDATE employee SET deleted = 't' WHERE login = ?|, $login) if $u_dbh && $user_db_exists;
562 $::lxdebug->leave_sub;
565 # --------------------------------------
569 sub restore_session {
570 $main::lxdebug->enter_sub();
574 $session_id = $::request->{cgi}->cookie($self->get_session_cookie_name());
575 $session_id =~ s|[^0-9a-f]||g if $session_id;
577 $self->{SESSION} = { };
580 $main::lxdebug->leave_sub();
581 return $self->session_restore_result(SESSION_NONE());
584 my ($dbh, $query, $sth, $cookie, $ref, $form);
588 # Don't fail if the auth DB doesn't yet.
589 if (!( $dbh = $self->dbconnect(1) )) {
590 $::lxdebug->leave_sub;
591 return $self->session_restore_result(SESSION_NONE());
594 # Don't fail if the "auth" schema doesn't exist yet, e.g. if the
595 # admin is creating the session tables at the moment.
596 $query = qq|SELECT *, (mtime < (now() - '$self->{session_timeout}m'::interval)) AS is_expired FROM auth.session WHERE id = ?|;
598 if (!($sth = $dbh->prepare($query)) || !$sth->execute($session_id)) {
599 $sth->finish if $sth;
600 $::lxdebug->leave_sub;
601 return $self->session_restore_result(SESSION_NONE());
604 $cookie = $sth->fetchrow_hashref;
607 # The session ID provided is valid in the following cases:
608 # 1. session ID exists in the database
609 # 2. hasn't expired yet
610 # 3. if cookie for the API token is given: the cookie's value equal database column 'auth.session.api_token' for the session ID
611 # 4. if cookie for the API token is NOT given then: the requestee's IP address must match the stored IP address
612 $self->{api_token} = $cookie->{api_token} if $cookie;
613 my $api_token_cookie = $self->get_api_token_cookie;
614 my $cookie_is_bad = !$cookie || $cookie->{is_expired};
615 $cookie_is_bad ||= $api_token_cookie && ($api_token_cookie ne $cookie->{api_token}) if $api_token_cookie;
616 $cookie_is_bad ||= $cookie->{ip_address} ne $ENV{REMOTE_ADDR} if !$api_token_cookie;
617 if ($cookie_is_bad) {
618 $self->destroy_session();
619 $main::lxdebug->leave_sub();
620 return $self->session_restore_result($cookie ? SESSION_EXPIRED() : SESSION_NONE());
623 if ($self->{column_information}->has('auto_restore')) {
624 $self->_load_with_auto_restore_column($dbh, $session_id);
626 $self->_load_without_auto_restore_column($dbh, $session_id);
629 $main::lxdebug->leave_sub();
631 return $self->session_restore_result(SESSION_OK());
634 sub session_restore_result {
637 $self->{session_restore_result} = $_[0];
639 return $self->{session_restore_result};
642 sub _load_without_auto_restore_column {
643 my ($self, $dbh, $session_id) = @_;
646 SELECT sess_key, sess_value
647 FROM auth.session_content
648 WHERE (session_id = ?)
650 my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
652 while (my $ref = $sth->fetchrow_hashref) {
653 my $value = SL::Auth::SessionValue->new(auth => $self,
654 key => $ref->{sess_key},
655 value => $ref->{sess_value},
657 $self->{SESSION}->{ $ref->{sess_key} } = $value;
659 next if defined $::form->{$ref->{sess_key}};
661 my $data = $value->get;
662 $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
666 sub _load_with_auto_restore_column {
667 my ($self, $dbh, $session_id) = @_;
669 my $auto_restore_keys = join ', ', map { "'${_}'" } qw(login password rpw);
672 SELECT sess_key, sess_value, auto_restore
673 FROM auth.session_content
674 WHERE (session_id = ?)
676 OR sess_key IN (${auto_restore_keys}))
678 my $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
680 while (my $ref = $sth->fetchrow_hashref) {
681 my $value = SL::Auth::SessionValue->new(auth => $self,
682 key => $ref->{sess_key},
683 value => $ref->{sess_value},
684 auto_restore => $ref->{auto_restore},
686 $self->{SESSION}->{ $ref->{sess_key} } = $value;
688 next if defined $::form->{$ref->{sess_key}};
690 my $data = $value->get;
691 $::form->{$ref->{sess_key}} = $data if $value->{auto_restore} || !ref $data;
698 FROM auth.session_content
699 WHERE (session_id = ?)
700 AND NOT COALESCE(auto_restore, FALSE)
701 AND (sess_key NOT IN (${auto_restore_keys}))
703 $sth = prepare_execute_query($::form, $dbh, $query, $session_id);
705 while (my $ref = $sth->fetchrow_hashref) {
706 my $value = SL::Auth::SessionValue->new(auth => $self,
707 key => $ref->{sess_key});
708 $self->{SESSION}->{ $ref->{sess_key} } = $value;
712 sub destroy_session {
713 $main::lxdebug->enter_sub();
718 my $dbh = $self->dbconnect();
722 do_query($main::form, $dbh, qq|DELETE FROM auth.session_content WHERE session_id = ?|, $session_id);
723 do_query($main::form, $dbh, qq|DELETE FROM auth.session WHERE id = ?|, $session_id);
727 SL::SessionFile->destroy_session($session_id);
730 $self->{SESSION} = { };
733 $main::lxdebug->leave_sub();
736 sub active_session_ids {
738 my $dbh = $self->dbconnect;
740 my $query = qq|SELECT id FROM auth.session|;
742 my @ids = selectall_array_query($::form, $dbh, $query);
747 sub expire_sessions {
748 $main::lxdebug->enter_sub();
752 $main::lxdebug->leave_sub and return if !$self->session_tables_present;
754 my $dbh = $self->dbconnect();
756 my $query = qq|SELECT id
758 WHERE (mtime < (now() - '$self->{session_timeout}m'::interval))|;
760 my @ids = selectall_array_query($::form, $dbh, $query);
765 SL::SessionFile->destroy_session($_) for @ids;
767 $query = qq|DELETE FROM auth.session_content
768 WHERE session_id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
769 do_query($main::form, $dbh, $query, @ids);
771 $query = qq|DELETE FROM auth.session
772 WHERE id IN (| . join(', ', ('?') x scalar(@ids)) . qq|)|;
773 do_query($main::form, $dbh, $query, @ids);
778 $main::lxdebug->leave_sub();
781 sub _create_session_id {
782 $main::lxdebug->enter_sub();
785 map { push @data, int(rand() * 255); } (1..32);
787 my $id = md5_hex(pack 'C*', @data);
789 $main::lxdebug->leave_sub();
794 sub create_or_refresh_session {
795 $session_id ||= shift->_create_session_id;
799 $::lxdebug->enter_sub;
801 my $provided_dbh = shift;
803 my $dbh = $provided_dbh || $self->dbconnect(1);
805 $::lxdebug->leave_sub && return unless $dbh && $session_id;
807 $dbh->begin_work unless $provided_dbh;
809 # If this fails then the "auth" schema might not exist yet, e.g. if
810 # the admin is just trying to create the auth database.
811 if (!$dbh->do(qq|LOCK auth.session_content|)) {
812 $dbh->rollback unless $provided_dbh;
813 $::lxdebug->leave_sub;
817 my @unfetched_keys = map { $_->{key} }
818 grep { ! $_->{fetched} }
819 values %{ $self->{SESSION} };
820 # $::lxdebug->dump(0, "unfetched_keys", [ sort @unfetched_keys ]);
821 # $::lxdebug->dump(0, "all keys", [ sort map { $_->{key} } values %{ $self->{SESSION} } ]);
822 my $query = qq|DELETE FROM auth.session_content WHERE (session_id = ?)|;
823 $query .= qq| AND (sess_key NOT IN (| . join(', ', ('?') x scalar @unfetched_keys) . qq|))| if @unfetched_keys;
825 do_query($::form, $dbh, $query, $session_id, @unfetched_keys);
827 my ($id) = selectrow_query($::form, $dbh, qq|SELECT id FROM auth.session WHERE id = ?|, $session_id);
830 do_query($::form, $dbh, qq|UPDATE auth.session SET mtime = now() WHERE id = ?|, $session_id);
832 do_query($::form, $dbh, qq|INSERT INTO auth.session (id, ip_address, mtime) VALUES (?, ?, now())|, $session_id, $ENV{REMOTE_ADDR});
835 if ($self->{column_information}->has('api_token', 'session')) {
836 my ($stored_api_token) = $dbh->selectrow_array(qq|SELECT api_token FROM auth.session WHERE id = ?|, undef, $session_id);
837 do_query($::form, $dbh, qq|UPDATE auth.session SET api_token = ? WHERE id = ?|, $self->_create_session_id, $session_id) unless $stored_api_token;
840 my @values_to_save = grep { $_->{fetched} }
841 values %{ $self->{SESSION} };
842 if (@values_to_save) {
843 my ($columns, $placeholders) = ('', '');
844 my $auto_restore = $self->{column_information}->has('auto_restore');
847 $columns .= ', auto_restore';
848 $placeholders .= ', ?';
851 $query = qq|INSERT INTO auth.session_content (session_id, sess_key, sess_value ${columns}) VALUES (?, ?, ? ${placeholders})|;
852 my $sth = prepare_query($::form, $dbh, $query);
854 foreach my $value (@values_to_save) {
855 my @values = ($value->{key}, $value->get_dumped);
856 push @values, $value->{auto_restore} if $auto_restore;
858 do_statement($::form, $sth, $query, $session_id, @values);
864 $dbh->commit() unless $provided_dbh;
865 $::lxdebug->leave_sub;
868 sub set_session_value {
869 $main::lxdebug->enter_sub();
874 $self->{SESSION} ||= { };
877 my $key = shift @params;
879 if (ref $key eq 'HASH') {
880 $self->{SESSION}->{ $key->{key} } = SL::Auth::SessionValue->new(key => $key->{key},
881 value => $key->{value},
882 auto_restore => $key->{auto_restore});
885 my $value = shift @params;
886 $self->{SESSION}->{ $key } = SL::Auth::SessionValue->new(key => $key,
891 $main::lxdebug->leave_sub();
896 sub delete_session_value {
897 $main::lxdebug->enter_sub();
901 $self->{SESSION} ||= { };
902 delete @{ $self->{SESSION} }{ @_ };
904 $main::lxdebug->leave_sub();
909 sub get_session_value {
910 $main::lxdebug->enter_sub();
913 my $data = $self->{SESSION} && $self->{SESSION}->{ $_[0] } ? $self->{SESSION}->{ $_[0] }->get : undef;
915 $main::lxdebug->leave_sub();
920 sub create_unique_sesion_value {
921 my ($self, $value, %params) = @_;
923 $self->{SESSION} ||= { };
925 my @now = gettimeofday();
926 my $key = "$$-" . ($now[0] * 1000000 + $now[1]) . "-";
927 $self->{unique_counter} ||= 0;
931 $self->{unique_counter}++;
932 $hashed_key = md5_hex($key . $self->{unique_counter});
933 } while (exists $self->{SESSION}->{$hashed_key});
935 $self->set_session_value($hashed_key => $value);
940 sub save_form_in_session {
941 my ($self, %params) = @_;
943 my $form = delete($params{form}) || $::form;
944 my $non_scalars = delete $params{non_scalars};
947 my %skip_keys = map { ( $_ => 1 ) } (qw(login password stylesheet version titlebar), @{ $params{skip_keys} || [] });
949 foreach my $key (grep { !$skip_keys{$_} } keys %{ $form }) {
950 $data->{$key} = $form->{$key} if !ref($form->{$key}) || $non_scalars;
953 return $self->create_unique_sesion_value($data, %params);
956 sub restore_form_from_session {
957 my ($self, $key, %params) = @_;
959 my $data = $self->get_session_value($key);
960 return $self unless $data;
962 my $form = delete($params{form}) || $::form;
963 my $clobber = exists $params{clobber} ? $params{clobber} : 1;
965 map { $form->{$_} = $data->{$_} if $clobber || !exists $form->{$_} } keys %{ $data };
970 sub set_cookie_environment_variable {
972 $ENV{HTTP_COOKIE} = $self->get_session_cookie_name() . "=${session_id}";
975 sub get_session_cookie_name {
976 my ($self, %params) = @_;
978 $params{type} ||= 'id';
979 my $name = $self->{cookie_name} || 'lx_office_erp_session_id';
980 $name .= '_api_token' if $params{type} eq 'api_token';
989 sub get_api_token_cookie {
992 $::request->{cgi}->cookie($self->get_session_cookie_name(type => 'api_token'));
995 sub is_api_token_cookie_valid {
997 my $provided_api_token = $self->get_api_token_cookie;
998 return $self->{api_token} && $provided_api_token && ($self->{api_token} eq $provided_api_token);
1001 sub session_tables_present {
1002 $main::lxdebug->enter_sub();
1006 # Only re-check for the presence of auth tables if either the check
1007 # hasn't been done before of if they weren't present.
1008 if ($self->{session_tables_present}) {
1009 $main::lxdebug->leave_sub();
1010 return $self->{session_tables_present};
1013 my $dbh = $self->dbconnect(1);
1016 $main::lxdebug->leave_sub();
1023 WHERE (schemaname = 'auth')
1024 AND (tablename IN ('session', 'session_content'))|;
1026 my ($count) = selectrow_query($main::form, $dbh, $query);
1028 $self->{session_tables_present} = 2 == $count;
1030 $main::lxdebug->leave_sub();
1032 return $self->{session_tables_present};
1035 # --------------------------------------
1037 sub all_rights_full {
1038 my $locale = $main::locale;
1041 ["--crm", $locale->text("CRM optional software")],
1042 ["crm_search", $locale->text("CRM search")],
1043 ["crm_new", $locale->text("CRM create customers, vendors and contacts")],
1044 ["crm_service", $locale->text("CRM services")],
1045 ["crm_admin", $locale->text("CRM admin")],
1046 ["crm_adminuser", $locale->text("CRM user")],
1047 ["crm_adminstatus", $locale->text("CRM status")],
1048 ["crm_email", $locale->text("CRM send email")],
1049 ["crm_termin", $locale->text("CRM termin")],
1050 ["crm_opportunity", $locale->text("CRM opportunity")],
1051 ["crm_knowhow", $locale->text("CRM know how")],
1052 ["crm_follow", $locale->text("CRM follow up")],
1053 ["crm_notices", $locale->text("CRM notices")],
1054 ["crm_other", $locale->text("CRM other")],
1055 ["--master_data", $locale->text("Master Data")],
1056 ["customer_vendor_edit", $locale->text("Create customers and vendors. Edit all vendors. Edit only customers where salesman equals employee (login)")],
1057 ["customer_vendor_all_edit", $locale->text("Create customers and vendors. Edit all vendors. Edit all customers")],
1058 ["part_service_assembly_edit", $locale->text("Create and edit parts, services, assemblies")],
1059 ["part_service_assembly_details", $locale->text("Show details and reports of parts, services, assemblies")],
1060 ["project_edit", $locale->text("Create and edit projects")],
1061 ["--ar", $locale->text("AR")],
1062 ["sales_quotation_edit", $locale->text("Create and edit sales quotations")],
1063 ["sales_order_edit", $locale->text("Create and edit sales orders")],
1064 ["sales_delivery_order_edit", $locale->text("Create and edit sales delivery orders")],
1065 ["invoice_edit", $locale->text("Create and edit invoices and credit notes")],
1066 ["dunning_edit", $locale->text("Create and edit dunnings")],
1067 ["sales_all_edit", $locale->text("View/edit all employees sales documents")],
1068 ["edit_prices", $locale->text("Edit prices and discount (if not used, textfield is ONLY set readonly)")],
1069 ["show_ar_transactions", $locale->text("Show AR transactions as part of AR invoice report")],
1070 ["delivery_plan", $locale->text("Show delivery plan")],
1071 ["--ap", $locale->text("AP")],
1072 ["request_quotation_edit", $locale->text("Create and edit RFQs")],
1073 ["purchase_order_edit", $locale->text("Create and edit purchase orders")],
1074 ["purchase_delivery_order_edit", $locale->text("Create and edit purchase delivery orders")],
1075 ["vendor_invoice_edit", $locale->text("Create and edit vendor invoices")],
1076 ["show_ap_transactions", $locale->text("Show AP transactions as part of AP invoice report")],
1077 ["--warehouse_management", $locale->text("Warehouse management")],
1078 ["warehouse_contents", $locale->text("View warehouse content")],
1079 ["warehouse_management", $locale->text("Warehouse management")],
1080 ["--general_ledger_cash", $locale->text("General ledger and cash")],
1081 ["general_ledger", $locale->text("Transactions, AR transactions, AP transactions")],
1082 ["datev_export", $locale->text("DATEV Export")],
1083 ["cash", $locale->text("Receipt, payment, reconciliation")],
1084 ["--reports", $locale->text('Reports')],
1085 ["report", $locale->text('All reports')],
1086 ["advance_turnover_tax_return", $locale->text('Advance turnover tax return')],
1087 ["--batch_printing", $locale->text("Batch Printing")],
1088 ["batch_printing", $locale->text("Batch Printing")],
1089 ["--configuration", $locale->text("Configuration")],
1090 ["config", $locale->text("Change kivitendo installation settings (most entries in the 'System' menu)")],
1091 ["admin", $locale->text("Client administration: configuration, editing templates, task server control, background jobs (remaining entries in the 'System' menu)")],
1092 ["--others", $locale->text("Others")],
1093 ["email_bcc", $locale->text("May set the BCC field when sending emails")],
1094 ["productivity", $locale->text("Productivity")],
1095 ["display_admin_link", $locale->text("Show administration link")],
1102 return grep !/^--/, map { $_->[0] } all_rights_full();
1106 $main::lxdebug->enter_sub();
1110 my $form = $main::form;
1112 my $dbh = $self->dbconnect();
1114 my $query = 'SELECT * FROM auth."group"';
1115 my $sth = prepare_execute_query($form, $dbh, $query);
1119 while ($row = $sth->fetchrow_hashref()) {
1120 $groups->{$row->{id}} = $row;
1124 $query = 'SELECT * FROM auth.user_group WHERE group_id = ?';
1125 $sth = prepare_query($form, $dbh, $query);
1127 foreach $group (values %{$groups}) {
1130 do_statement($form, $sth, $query, $group->{id});
1132 while ($row = $sth->fetchrow_hashref()) {
1133 push @members, $row->{user_id};
1135 $group->{members} = [ uniq @members ];
1139 $query = 'SELECT * FROM auth.group_rights WHERE group_id = ?';
1140 $sth = prepare_query($form, $dbh, $query);
1142 foreach $group (values %{$groups}) {
1143 $group->{rights} = {};
1145 do_statement($form, $sth, $query, $group->{id});
1147 while ($row = $sth->fetchrow_hashref()) {
1148 $group->{rights}->{$row->{right}} |= $row->{granted};
1151 map { $group->{rights}->{$_} = 0 if (!defined $group->{rights}->{$_}); } all_rights();
1155 $main::lxdebug->leave_sub();
1161 $main::lxdebug->enter_sub();
1166 my $form = $main::form;
1167 my $dbh = $self->dbconnect();
1171 my ($query, $sth, $row, $rights);
1173 if (!$group->{id}) {
1174 ($group->{id}) = selectrow_query($form, $dbh, qq|SELECT nextval('auth.group_id_seq')|);
1176 $query = qq|INSERT INTO auth."group" (id, name, description) VALUES (?, '', '')|;
1177 do_query($form, $dbh, $query, $group->{id});
1180 do_query($form, $dbh, qq|UPDATE auth."group" SET name = ?, description = ? WHERE id = ?|, map { $group->{$_} } qw(name description id));
1182 do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $group->{id});
1184 $query = qq|INSERT INTO auth.user_group (user_id, group_id) VALUES (?, ?)|;
1185 $sth = prepare_query($form, $dbh, $query);
1187 foreach my $user_id (uniq @{ $group->{members} }) {
1188 do_statement($form, $sth, $query, $user_id, $group->{id});
1192 do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $group->{id});
1194 $query = qq|INSERT INTO auth.group_rights (group_id, "right", granted) VALUES (?, ?, ?)|;
1195 $sth = prepare_query($form, $dbh, $query);
1197 foreach my $right (keys %{ $group->{rights} }) {
1198 do_statement($form, $sth, $query, $group->{id}, $right, $group->{rights}->{$right} ? 't' : 'f');
1204 $main::lxdebug->leave_sub();
1208 $main::lxdebug->enter_sub();
1213 my $form = $main::form;
1215 my $dbh = $self->dbconnect();
1218 do_query($form, $dbh, qq|DELETE FROM auth.user_group WHERE group_id = ?|, $id);
1219 do_query($form, $dbh, qq|DELETE FROM auth.group_rights WHERE group_id = ?|, $id);
1220 do_query($form, $dbh, qq|DELETE FROM auth."group" WHERE id = ?|, $id);
1224 $main::lxdebug->leave_sub();
1227 sub evaluate_rights_ary {
1228 $main::lxdebug->enter_sub(2);
1235 foreach my $el (@{$ary}) {
1236 if (ref $el eq "ARRAY") {
1237 if ($action eq '|') {
1238 $value |= evaluate_rights_ary($el);
1240 $value &= evaluate_rights_ary($el);
1243 } elsif (($el eq '&') || ($el eq '|')) {
1246 } elsif ($action eq '|') {
1255 $main::lxdebug->leave_sub(2);
1260 sub _parse_rights_string {
1261 $main::lxdebug->enter_sub(2);
1271 push @stack, $cur_ary;
1273 while ($access =~ m/^([a-z_0-9]+|\||\&|\(|\)|\s+)/) {
1275 substr($access, 0, length $1) = "";
1277 next if ($token =~ /\s/);
1279 if ($token eq "(") {
1280 my $new_cur_ary = [];
1281 push @stack, $new_cur_ary;
1282 push @{$cur_ary}, $new_cur_ary;
1283 $cur_ary = $new_cur_ary;
1285 } elsif ($token eq ")") {
1289 $main::lxdebug->leave_sub(2);
1293 $cur_ary = $stack[-1];
1295 } elsif (($token eq "|") || ($token eq "&")) {
1296 push @{$cur_ary}, $token;
1299 push @{$cur_ary}, $self->{RIGHTS}->{$login}->{$token} * 1;
1303 my $result = ($access || (1 < scalar @stack)) ? 0 : evaluate_rights_ary($stack[0]);
1305 $main::lxdebug->leave_sub(2);
1311 $main::lxdebug->enter_sub(2);
1316 my $default = shift;
1318 $self->{FULL_RIGHTS} ||= { };
1319 $self->{FULL_RIGHTS}->{$login} ||= { };
1321 if (!defined $self->{FULL_RIGHTS}->{$login}->{$right}) {
1322 $self->{RIGHTS} ||= { };
1323 $self->{RIGHTS}->{$login} ||= $self->load_rights_for_user($login);
1325 $self->{FULL_RIGHTS}->{$login}->{$right} = $self->_parse_rights_string($login, $right);
1328 my $granted = $self->{FULL_RIGHTS}->{$login}->{$right};
1329 $granted = $default if (!defined $granted);
1331 $main::lxdebug->leave_sub(2);
1337 $::lxdebug->enter_sub(2);
1338 my ($self, $right, $dont_abort) = @_;
1340 if ($self->check_right($::myconfig{login}, $right)) {
1341 $::lxdebug->leave_sub(2);
1346 delete $::form->{title};
1347 $::form->show_generic_error($::locale->text("You do not have the permissions to access this function."));
1350 $::lxdebug->leave_sub(2);
1355 sub load_rights_for_user {
1356 $::lxdebug->enter_sub;
1358 my ($self, $login) = @_;
1359 my $dbh = $self->dbconnect;
1360 my ($query, $sth, $row, $rights);
1362 $rights = { map { $_ => 0 } all_rights() };
1365 qq|SELECT gr."right", gr.granted
1366 FROM auth.group_rights gr
1369 FROM auth.user_group ug
1370 LEFT JOIN auth."user" u ON (ug.user_id = u.id)
1374 FROM auth.clients_groups cg
1375 WHERE cg.client_id = ?)|;
1377 $sth = prepare_execute_query($::form, $dbh, $query, $login, $self->client->{id});
1379 while ($row = $sth->fetchrow_hashref()) {
1380 $rights->{$row->{right}} |= $row->{granted};
1384 $::lxdebug->leave_sub;
1398 SL::Auth - Authentication and session handling
1404 =item C<set_session_value @values>
1406 =item C<set_session_value %values>
1408 Store all values of C<@values> or C<%values> in the session. Each
1409 member of C<@values> is tested if it is a hash reference. If it is
1410 then it must contain the keys C<key> and C<value> and can optionally
1411 contain the key C<auto_restore>. In this case C<value> is associated
1412 with C<key> and restored to C<$::form> upon the next request
1413 automatically if C<auto_restore> is trueish or if C<value> is a scalar
1416 If the current member of C<@values> is not a hash reference then it
1417 will be used as the C<key> and the next entry of C<@values> is used as
1418 the C<value> to store. In this case setting C<auto_restore> is not
1421 Therefore the following two invocations are identical:
1423 $::auth-E<gt>set_session_value(name =E<gt> "Charlie");
1424 $::auth-E<gt>set_session_value({ key =E<gt> "name", value =E<gt> "Charlie" });
1426 All of these values are copied back into C<$::form> for the next
1427 request automatically if they're scalar values or if they have
1428 C<auto_restore> set to trueish.
1430 The values can be any Perl structure. They are stored as YAML dumps.
1432 =item C<get_session_value $key>
1434 Retrieve a value from the session. Returns C<undef> if the value
1437 =item C<create_unique_sesion_value $value, %params>
1439 Create a unique key in the session and store C<$value>
1442 Returns the key created in the session.
1444 =item C<save_session>
1446 Stores the session values in the database. This is the only function
1447 that actually stores stuff in the database. Neither the various
1448 setters nor the deleter access the database.
1450 =item <save_form_in_session %params>
1452 Stores the content of C<$params{form}> (default: C<$::form>) in the
1453 session using L</create_unique_sesion_value>.
1455 If C<$params{non_scalars}> is trueish then non-scalar values will be
1456 stored as well. Default is to only store scalar values.
1458 The following keys will never be saved: C<login>, C<password>,
1459 C<stylesheet>, C<titlebar>, C<version>. Additional keys not to save
1460 can be given as an array ref in C<$params{skip_keys}>.
1462 Returns the unique key under which the form is stored.
1464 =item <restore_form_from_session $key, %params>
1466 Restores the form from the session into C<$params{form}> (default:
1469 If C<$params{clobber}> is falsish then existing values with the same
1470 key in C<$params{form}> will not be overwritten. C<$params{clobber}>
1483 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>