From 04e4fe46f2ba5686659974e58baba9150572d7db Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Wed, 22 Jun 2022 13:52:47 +0900 Subject: [PATCH] Update ACL\Login class with _GET/_POST login parameter loginUserId parameter in _GET or _POST for direct login without username and password. This can be secured by: - must login after x days from set loginUserId on - can only login with loginUserId in given time range - flag lock loginUserId --- 4dev/database/ORDER | 1 + 4dev/database/database_create_data.sql | 205 +++-- .../database/function/edit_access_set_uid.sql | 38 +- 4dev/database/function/edit_group_set_uid.sql | 38 +- .../edit_user_set_login_user_id_set_date.sql | 24 + 4dev/database/function/random_string.sql | 8 +- 4dev/database/function/set_date.sql | 23 +- 4dev/database/function/set_edit_generic.sql | 29 +- 4dev/database/function/set_generic_uid.sql | 31 +- 4dev/database/function/set_uid.sql | 23 +- 4dev/database/function/update_function.sql | 27 +- 4dev/database/table/edit_user.sql | 60 +- 4dev/database/trigger/trg_edit_user.sql | 5 + ... => 20190910-edit_update_missing_cuid.sql} | 0 ... 20191211-edit_tables_missing_columns.sql} | 0 .../20220617-edit_user_login_user_id_add.sql | 47 ++ 4dev/tests/CoreLibsACLLoginTest.php | 712 +++++++++++++++++- .../CoreLibsACLLogin_database_prepare.sh | 10 +- .../CoreLibsACLLogin_database_create_data.sql | 205 +++-- www/admin/edit_groups_test.php | 61 ++ www/lib/CoreLibs/ACL/Login.php | 330 ++++++-- 21 files changed, 1597 insertions(+), 280 deletions(-) create mode 100644 4dev/database/function/edit_user_set_login_user_id_set_date.sql rename 4dev/database/update/{edit_update_missing_cuid.sql => 20190910-edit_update_missing_cuid.sql} (100%) rename 4dev/database/update/{edit_tables_missing_columns.sql => 20191211-edit_tables_missing_columns.sql} (100%) create mode 100644 4dev/database/update/20220617-edit_user_login_user_id_add.sql create mode 100644 www/admin/edit_groups_test.php diff --git a/4dev/database/ORDER b/4dev/database/ORDER index 4412dd35..eb53916c 100644 --- a/4dev/database/ORDER +++ b/4dev/database/ORDER @@ -5,6 +5,7 @@ function/set_edit_generic.sql function/edit_access_set_uid.sql function/edit_group_set_uid.sql function/edit_log_partition_insert.sql +function/edit_user_set_login_user_id_set_date.sql # generic tables table/edit_temp_files.sql table/edit_generic.sql diff --git a/4dev/database/database_create_data.sql b/4dev/database/database_create_data.sql index 913ba022..d8f4bc61 100644 --- a/4dev/database/database_create_data.sql +++ b/4dev/database/database_create_data.sql @@ -2,7 +2,8 @@ -- create random string with length X CREATE FUNCTION random_string(randomLength int) -RETURNS text AS $$ +RETURNS text AS +$$ SELECT array_to_string( ARRAY( SELECT substring( @@ -14,53 +15,58 @@ SELECT array_to_string( ), '' ) -$$ LANGUAGE SQL +$$ +LANGUAGE SQL RETURNS NULL ON NULL INPUT -VOLATILE; -- LEAKPROOF;-- END: function/random_string.sql +VOLATILE; -- LEAKPROOF; +-- END: function/random_string.sql -- START: function/set_edit_generic.sql -- adds the created or updated date tags -CREATE OR REPLACE FUNCTION set_edit_generic() RETURNS TRIGGER AS ' - DECLARE - random_length INT = 12; -- that should be long enough - BEGIN - IF TG_OP = ''INSERT'' THEN - NEW.date_created := ''now''; - NEW.cuid := random_string(random_length); - ELSIF TG_OP = ''UPDATE'' THEN - NEW.date_updated := ''now''; - END IF; - RETURN NEW; - END; -' LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION set_edit_generic() +RETURNS TRIGGER AS +$$ +DECLARE + random_length INT = 12; -- that should be long enough +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.date_created := 'now'; + NEW.cuid := random_string(random_length); + ELSIF TG_OP = 'UPDATE' THEN + NEW.date_updated := 'now'; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; -- END: function/set_edit_generic.sql -- START: function/edit_access_set_uid.sql -- add uid add for edit_access table CREATE OR REPLACE FUNCTION set_edit_access_uid() RETURNS TRIGGER AS $$ - DECLARE - myrec RECORD; - v_uid VARCHAR; - BEGIN - -- skip if NEW.name is not set - IF NEW.name IS NOT NULL AND NEW.name <> '' THEN - -- use NEW.name as base, remove all spaces - -- name data is already unique, so we do not need to worry about this here - v_uid := REPLACE(NEW.name, ' ', ''); - IF TG_OP = 'INSERT' THEN - -- always set +DECLARE + myrec RECORD; + v_uid VARCHAR; +BEGIN + -- skip if NEW.name is not set + IF NEW.name IS NOT NULL AND NEW.name <> '' THEN + -- use NEW.name as base, remove all spaces + -- name data is already unique, so we do not need to worry about this here + v_uid := REPLACE(NEW.name, ' ', ''); + IF TG_OP = 'INSERT' THEN + -- always set + NEW.uid := v_uid; + ELSIF TG_OP = 'UPDATE' THEN + -- check if not set, then set + SELECT INTO myrec t.* FROM edit_access t WHERE edit_access_id = NEW.edit_access_id; + IF FOUND THEN NEW.uid := v_uid; - ELSIF TG_OP = 'UPDATE' THEN - -- check if not set, then set - SELECT INTO myrec t.* FROM edit_access t WHERE edit_access_id = NEW.edit_access_id; - IF FOUND THEN - NEW.uid := v_uid; - END IF; END IF; END IF; - RETURN NEW; - END; + END IF; + RETURN NEW; +END; $$ LANGUAGE 'plpgsql'; -- END: function/edit_access_set_uid.sql @@ -69,28 +75,28 @@ $$ CREATE OR REPLACE FUNCTION set_edit_group_uid() RETURNS TRIGGER AS $$ - DECLARE - myrec RECORD; - v_uid VARCHAR; - BEGIN - -- skip if NEW.name is not set - IF NEW.name IS NOT NULL AND NEW.name <> '' THEN - -- use NEW.name as base, remove all spaces - -- name data is already unique, so we do not need to worry about this here - v_uid := REPLACE(NEW.name, ' ', ''); - IF TG_OP = 'INSERT' THEN - -- always set +DECLARE + myrec RECORD; + v_uid VARCHAR; +BEGIN + -- skip if NEW.name is not set + IF NEW.name IS NOT NULL AND NEW.name <> '' THEN + -- use NEW.name as base, remove all spaces + -- name data is already unique, so we do not need to worry about this here + v_uid := REPLACE(NEW.name, ' ', ''); + IF TG_OP = 'INSERT' THEN + -- always set + NEW.uid := v_uid; + ELSIF TG_OP = 'UPDATE' THEN + -- check if not set, then set + SELECT INTO myrec t.* FROM edit_group t WHERE edit_group_id = NEW.edit_group_id; + IF FOUND THEN NEW.uid := v_uid; - ELSIF TG_OP = 'UPDATE' THEN - -- check if not set, then set - SELECT INTO myrec t.* FROM edit_group t WHERE edit_group_id = NEW.edit_group_id; - IF FOUND THEN - NEW.uid := v_uid; - END IF; END IF; END IF; - RETURN NEW; - END; + END IF; + RETURN NEW; +END; $$ LANGUAGE 'plpgsql'; -- END: function/edit_group_set_uid.sql @@ -246,6 +252,32 @@ END $$ LANGUAGE 'plpgsql'; -- END: function/edit_log_partition_insert.sql +-- START: function/edit_user_set_login_user_id_set_date.sql +-- set edit user login_user_id_set_date if login_user_id is set +-- NOW() if not empty + +CREATE OR REPLACE FUNCTION set_login_user_id_set_date() +RETURNS TRIGGER AS +$$ +BEGIN + -- if new is not null/empty + -- and old one is null or old one different new one + -- set NOW() + -- if new one is NULL + -- set NULL + IF + NEW.login_user_id IS NOT NULL AND NEW.login_user_id <> '' AND + (OLD.login_user_id IS NULL OR NEW.login_user_id <> OLD.login_user_id) + THEN + NEW.login_user_id_set_date = NOW(); + ELSIF NEW.login_user_id IS NULL OR NEW.login_user_id = '' THEN + NEW.login_user_id_set_date = NULL; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; +-- END: function/edit_user_set_login_user_id_set_date.sql -- START: table/edit_temp_files.sql -- AUTHOR: Clemens Schwaighofer -- DATE: 2005/07/08 @@ -526,34 +558,80 @@ CREATE TABLE edit_user ( FOREIGN KEY (edit_scheme_id) REFERENCES edit_scheme (edit_scheme_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_access_right_id INT NOT NULL, FOREIGN KEY (edit_access_right_id) REFERENCES edit_access_right (edit_access_right_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, - enabled SMALLINT NOT NULL DEFAULT 0, - deleted SMALLINT NOT NULL DEFAULT 0, + -- username/password username VARCHAR UNIQUE, password VARCHAR, + -- name block first_name VARCHAR, last_name VARCHAR, first_name_furigana VARCHAR, last_name_furigana VARCHAR, + -- email + email VARCHAR, + -- eanbled/deleted flag + enabled SMALLINT NOT NULL DEFAULT 0, + deleted SMALLINT NOT NULL DEFAULT 0, + -- general flags + strict SMALLINT DEFAULT 0, + locked SMALLINT DEFAULT 0, + protected SMALLINT NOT NULL DEFAULT 0, + -- legacy, debug flags debug SMALLINT NOT NULL DEFAULT 0, db_debug SMALLINT NOT NULL DEFAULT 0, - email VARCHAR, - protected SMALLINT NOT NULL DEFAULT 0, + -- is admin user admin SMALLINT NOT NULL DEFAULT 0, + -- last login log last_login TIMESTAMP WITHOUT TIME ZONE, + -- login error login_error_count INT DEFAULT 0, login_error_date_last TIMESTAMP WITHOUT TIME ZONE, login_error_date_first TIMESTAMP WITHOUT TIME ZONE, - strict SMALLINT DEFAULT 0, - locked SMALLINT DEFAULT 0, + -- time locked + lock_until TIMESTAMP WITHOUT TIME ZONE, + lock_after TIMESTAMP WITHOUT TIME ZONE, + -- password change password_change_date TIMESTAMP WITHOUT TIME ZONE, -- only when password is first set or changed password_change_interval INTERVAL, -- null if no change is needed, or d/m/y time interval password_reset_time TIMESTAMP WITHOUT TIME ZONE, -- when the password reset was requested password_reset_uid VARCHAR, -- the uid to access the password reset page + -- _GET login id for direct login + login_user_id VARCHAR, -- the login uid, at least 32 chars + login_user_id_set_date TIMESTAMP WITHOUT TIME ZONE, -- when above uid was set + login_user_id_valid_from TIMESTAMP WITHOUT TIME ZONE, -- if set, from when the above uid is valid + login_user_id_valid_until TIMESTAMP WITHOUT TIME ZONE, -- if set, until when the above uid is valid + login_user_id_revalidate_after INTERVAL DEFAULT '0 days', -- user must login to revalidated login id after set days, 0 for forever + login_user_id_locked SMALLINT DEFAULT 0, -- lock for login user id, but still allow normal login + -- additional ACL json block additional_acl JSONB -- additional ACL as JSON string (can be set by other pages) ) INHERITS (edit_generic) WITHOUT OIDS; +COMMENT ON COLUMN edit_user.username IS 'Login username, must set'; +COMMENT ON COLUMN edit_user.password IS 'Login password, must set'; +COMMENT ON COLUMN edit_user.enabled IS 'Login is enabled (master switch)'; +COMMENT ON COLUMN edit_user.deleted IS 'Login is deleted (master switch), overrides all other'; +COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be locked, default off'; +COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; +COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; +COMMENT ON COLUMN edit_user.debug IS 'Turn debug flag on (legacy)'; +COMMENT ON COLUMN edit_user.db_debug IS 'Turn DB debug flag on (legacy)'; +COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; +COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; +COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; +COMMENT ON COLUMN edit_user.login_error_date_last IS 'Last login error date'; +COMMENT ON COLUMN edit_user.login_error_date_first IS 'First login error date, reset on successfull login'; +COMMENT ON COLUMN edit_user.lock_until IS 'Account is locked until this date, <'; +COMMENT ON COLUMN edit_user.lock_after IS 'Account is locked after this date, >'; +COMMENT ON COLUMN edit_user.password_change_date IS 'Password was changed on'; +COMMENT ON COLUMN edit_user.password_change_interval IS 'After how many days the password has to be changed'; COMMENT ON COLUMN edit_user.password_reset_time IS 'When the password reset was requested. For reset page uid valid check'; -COMMENT ON COLUMN edit_user.password_reset_uid IS 'Password reset page uid'; +COMMENT ON COLUMN edit_user.password_reset_uid IS 'Password reset page uid, one time, invalid after reset successful or time out'; +COMMENT ON COLUMN edit_user.login_user_id IS 'Min 32 character UID to be used to login without password. Via GET/POST parameter'; +COMMENT ON COLUMN edit_user.login_user_id_set_date IS 'login id was set at what date'; +COMMENT ON COLUMN edit_user.login_user_id_valid_from IS 'login id is valid from this date, >='; +COMMENT ON COLUMN edit_user.login_user_id_valid_until IS 'login id is valid until this date, <='; +COMMENT ON COLUMN edit_user.login_user_id_revalidate_after IS 'If set to a number greater 0 then user must login after given amount of days to revalidate, set to 0 for valid forver'; +COMMENT ON COLUMN edit_user.login_user_id_locked IS 'A separte lock flag for login id, user can still login normal'; +COMMENT ON COLUMN edit_user.additional_acl IS 'Additional Access Control List stored in JSON format'; -- END: table/edit_user.sql -- START: table/edit_log.sql -- AUTHOR: Clemens Schwaighofer @@ -774,6 +852,11 @@ FOR EACH ROW EXECUTE PROCEDURE set_edit_generic(); CREATE TRIGGER trg_edit_user BEFORE INSERT OR UPDATE ON edit_user FOR EACH ROW EXECUTE PROCEDURE set_edit_generic(); + +-- DROP TRIGGER IF EXISTS trg_edit_user_set_login_user_id_set_date ON edit_user; +CREATE TRIGGER trg_edit_user_set_login_user_id_set_date +BEFORE INSERT OR UPDATE ON edit_user +FOR EACH ROW EXECUTE PROCEDURE set_login_user_id_set_date(); -- END: trigger/trg_edit_user.sql -- START: trigger/trg_edit_visible_group.sql -- DROP TRIGGER IF EXISTS trg_edit_visible_group ON edit_visible_group; diff --git a/4dev/database/function/edit_access_set_uid.sql b/4dev/database/function/edit_access_set_uid.sql index 89d4b053..a6d21e91 100644 --- a/4dev/database/function/edit_access_set_uid.sql +++ b/4dev/database/function/edit_access_set_uid.sql @@ -2,27 +2,27 @@ CREATE OR REPLACE FUNCTION set_edit_access_uid() RETURNS TRIGGER AS $$ - DECLARE - myrec RECORD; - v_uid VARCHAR; - BEGIN - -- skip if NEW.name is not set - IF NEW.name IS NOT NULL AND NEW.name <> '' THEN - -- use NEW.name as base, remove all spaces - -- name data is already unique, so we do not need to worry about this here - v_uid := REPLACE(NEW.name, ' ', ''); - IF TG_OP = 'INSERT' THEN - -- always set +DECLARE + myrec RECORD; + v_uid VARCHAR; +BEGIN + -- skip if NEW.name is not set + IF NEW.name IS NOT NULL AND NEW.name <> '' THEN + -- use NEW.name as base, remove all spaces + -- name data is already unique, so we do not need to worry about this here + v_uid := REPLACE(NEW.name, ' ', ''); + IF TG_OP = 'INSERT' THEN + -- always set + NEW.uid := v_uid; + ELSIF TG_OP = 'UPDATE' THEN + -- check if not set, then set + SELECT INTO myrec t.* FROM edit_access t WHERE edit_access_id = NEW.edit_access_id; + IF FOUND THEN NEW.uid := v_uid; - ELSIF TG_OP = 'UPDATE' THEN - -- check if not set, then set - SELECT INTO myrec t.* FROM edit_access t WHERE edit_access_id = NEW.edit_access_id; - IF FOUND THEN - NEW.uid := v_uid; - END IF; END IF; END IF; - RETURN NEW; - END; + END IF; + RETURN NEW; +END; $$ LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/edit_group_set_uid.sql b/4dev/database/function/edit_group_set_uid.sql index 8f957bd0..bcee3b11 100755 --- a/4dev/database/function/edit_group_set_uid.sql +++ b/4dev/database/function/edit_group_set_uid.sql @@ -2,27 +2,27 @@ CREATE OR REPLACE FUNCTION set_edit_group_uid() RETURNS TRIGGER AS $$ - DECLARE - myrec RECORD; - v_uid VARCHAR; - BEGIN - -- skip if NEW.name is not set - IF NEW.name IS NOT NULL AND NEW.name <> '' THEN - -- use NEW.name as base, remove all spaces - -- name data is already unique, so we do not need to worry about this here - v_uid := REPLACE(NEW.name, ' ', ''); - IF TG_OP = 'INSERT' THEN - -- always set +DECLARE + myrec RECORD; + v_uid VARCHAR; +BEGIN + -- skip if NEW.name is not set + IF NEW.name IS NOT NULL AND NEW.name <> '' THEN + -- use NEW.name as base, remove all spaces + -- name data is already unique, so we do not need to worry about this here + v_uid := REPLACE(NEW.name, ' ', ''); + IF TG_OP = 'INSERT' THEN + -- always set + NEW.uid := v_uid; + ELSIF TG_OP = 'UPDATE' THEN + -- check if not set, then set + SELECT INTO myrec t.* FROM edit_group t WHERE edit_group_id = NEW.edit_group_id; + IF FOUND THEN NEW.uid := v_uid; - ELSIF TG_OP = 'UPDATE' THEN - -- check if not set, then set - SELECT INTO myrec t.* FROM edit_group t WHERE edit_group_id = NEW.edit_group_id; - IF FOUND THEN - NEW.uid := v_uid; - END IF; END IF; END IF; - RETURN NEW; - END; + END IF; + RETURN NEW; +END; $$ LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/edit_user_set_login_user_id_set_date.sql b/4dev/database/function/edit_user_set_login_user_id_set_date.sql new file mode 100644 index 00000000..6f276287 --- /dev/null +++ b/4dev/database/function/edit_user_set_login_user_id_set_date.sql @@ -0,0 +1,24 @@ +-- set edit user login_user_id_set_date if login_user_id is set +-- NOW() if not empty + +CREATE OR REPLACE FUNCTION set_login_user_id_set_date() +RETURNS TRIGGER AS +$$ +BEGIN + -- if new is not null/empty + -- and old one is null or old one different new one + -- set NOW() + -- if new one is NULL + -- set NULL + IF + NEW.login_user_id IS NOT NULL AND NEW.login_user_id <> '' AND + (OLD.login_user_id IS NULL OR NEW.login_user_id <> OLD.login_user_id) + THEN + NEW.login_user_id_set_date = NOW(); + ELSIF NEW.login_user_id IS NULL OR NEW.login_user_id = '' THEN + NEW.login_user_id_set_date = NULL; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/random_string.sql b/4dev/database/function/random_string.sql index 5a0a6c5d..99def8c1 100644 --- a/4dev/database/function/random_string.sql +++ b/4dev/database/function/random_string.sql @@ -1,7 +1,8 @@ -- create random string with length X CREATE FUNCTION random_string(randomLength int) -RETURNS text AS $$ +RETURNS text AS +$$ SELECT array_to_string( ARRAY( SELECT substring( @@ -13,6 +14,7 @@ SELECT array_to_string( ), '' ) -$$ LANGUAGE SQL +$$ +LANGUAGE SQL RETURNS NULL ON NULL INPUT -VOLATILE; -- LEAKPROOF; \ No newline at end of file +VOLATILE; -- LEAKPROOF; diff --git a/4dev/database/function/set_date.sql b/4dev/database/function/set_date.sql index 27ef36e7..b7aee53a 100644 --- a/4dev/database/function/set_date.sql +++ b/4dev/database/function/set_date.sql @@ -1,12 +1,15 @@ -- adds the created or updated date tags -CREATE OR REPLACE FUNCTION set_date() RETURNS TRIGGER AS ' - BEGIN - IF TG_OP = ''INSERT'' THEN - NEW.date_created := ''now''; - ELSIF TG_OP = ''UPDATE'' THEN - NEW.date_updated := ''now''; - END IF; - RETURN NEW; - END; -' LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION set_date() +RETURNS TRIGGER AS +$$ +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.date_created := 'now'; + ELSIF TG_OP = 'UPDATE' THEN + NEW.date_updated := 'now'; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/set_edit_generic.sql b/4dev/database/function/set_edit_generic.sql index 3a5e9aaf..524fa0e3 100644 --- a/4dev/database/function/set_edit_generic.sql +++ b/4dev/database/function/set_edit_generic.sql @@ -1,15 +1,18 @@ -- adds the created or updated date tags -CREATE OR REPLACE FUNCTION set_edit_generic() RETURNS TRIGGER AS ' - DECLARE - random_length INT = 12; -- that should be long enough - BEGIN - IF TG_OP = ''INSERT'' THEN - NEW.date_created := ''now''; - NEW.cuid := random_string(random_length); - ELSIF TG_OP = ''UPDATE'' THEN - NEW.date_updated := ''now''; - END IF; - RETURN NEW; - END; -' LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION set_edit_generic() +RETURNS TRIGGER AS +$$ +DECLARE + random_length INT = 12; -- that should be long enough +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.date_created := 'now'; + NEW.cuid := random_string(random_length); + ELSIF TG_OP = 'UPDATE' THEN + NEW.date_updated := 'now'; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/set_generic_uid.sql b/4dev/database/function/set_generic_uid.sql index 97726e30..ef651bd2 100644 --- a/4dev/database/function/set_generic_uid.sql +++ b/4dev/database/function/set_generic_uid.sql @@ -1,18 +1,21 @@ -- set generic with date and uid combined -- don't use with set_generic/set_uid together -CREATE OR REPLACE FUNCTION set_generic() RETURNS TRIGGER AS ' - DECLARE - random_length INT = 32; -- long for massive data - BEGIN - IF TG_OP = ''INSERT'' THEN - NEW.date_created := ''now''; - IF NEW.uid IS NULL THEN - NEW.uid := random_string(random_length); - END IF; - ELSIF TG_OP = ''UPDATE'' THEN - NEW.date_updated := ''now''; +CREATE OR REPLACE FUNCTION set_generic() +RETURNS TRIGGER AS +$$ +DECLARE + random_length INT = 32; -- long for massive data +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.date_created := 'now'; + IF NEW.uid IS NULL THEN + NEW.uid := random_string(random_length); END IF; - RETURN NEW; - END; -' LANGUAGE 'plpgsql'; + ELSIF TG_OP = 'UPDATE' THEN + NEW.date_updated := 'now'; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/set_uid.sql b/4dev/database/function/set_uid.sql index c8cfb2bc..a1715d3a 100644 --- a/4dev/database/function/set_uid.sql +++ b/4dev/database/function/set_uid.sql @@ -1,12 +1,15 @@ -- adds the created or updated date tags -CREATE OR REPLACE FUNCTION set_uid() RETURNS TRIGGER AS ' - DECLARE - random_length INT = 32; -- that should be long enough - BEGIN - IF TG_OP = ''INSERT'' THEN - NEW.uid := random_string(random_length); - END IF; - RETURN NEW; - END; -' LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION set_uid() +RETURNS TRIGGER AS +$$ +DECLARE + random_length INT = 32; -- that should be long enough +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.uid := random_string(random_length); + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; diff --git a/4dev/database/function/update_function.sql b/4dev/database/function/update_function.sql index d0239759..12b76591 100644 --- a/4dev/database/function/update_function.sql +++ b/4dev/database/function/update_function.sql @@ -2,15 +2,18 @@ -- OLD, DEPRECATED, use set_generic.sql --- CREATE OR REPLACE FUNCTION set_generic() RETURNS TRIGGER AS ' --- BEGIN --- IF TG_OP = ''INSERT'' THEN --- NEW.date_created := clock_timestamp(); --- NEW.user_created := current_user; --- ELSIF TG_OP = ''UPDATE'' THEN --- NEW.date_updated := clock_timestamp(); --- NEW.user_updated := current_user; --- END IF; --- RETURN NEW; --- END; --- ' LANGUAGE 'plpgsql'; +-- CREATE OR REPLACE FUNCTION set_generic() +-- RETURNS TRIGGER AS +-- $$ +-- BEGIN +-- IF TG_OP = 'INSERT' THEN +-- NEW.date_created := clock_timestamp(); +-- NEW.user_created := current_user; +-- ELSIF TG_OP = 'UPDATE' THEN +-- NEW.date_updated := clock_timestamp(); +-- NEW.user_updated := current_user; +-- END IF; +-- RETURN NEW; +-- END; +-- $$ +-- LANGUAGE 'plpgsql'; diff --git a/4dev/database/table/edit_user.sql b/4dev/database/table/edit_user.sql index 7778c293..91b508f7 100644 --- a/4dev/database/table/edit_user.sql +++ b/4dev/database/table/edit_user.sql @@ -18,31 +18,77 @@ CREATE TABLE edit_user ( FOREIGN KEY (edit_scheme_id) REFERENCES edit_scheme (edit_scheme_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_access_right_id INT NOT NULL, FOREIGN KEY (edit_access_right_id) REFERENCES edit_access_right (edit_access_right_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, - enabled SMALLINT NOT NULL DEFAULT 0, - deleted SMALLINT NOT NULL DEFAULT 0, + -- username/password username VARCHAR UNIQUE, password VARCHAR, + -- name block first_name VARCHAR, last_name VARCHAR, first_name_furigana VARCHAR, last_name_furigana VARCHAR, + -- email + email VARCHAR, + -- eanbled/deleted flag + enabled SMALLINT NOT NULL DEFAULT 0, + deleted SMALLINT NOT NULL DEFAULT 0, + -- general flags + strict SMALLINT DEFAULT 0, + locked SMALLINT DEFAULT 0, + protected SMALLINT NOT NULL DEFAULT 0, + -- legacy, debug flags debug SMALLINT NOT NULL DEFAULT 0, db_debug SMALLINT NOT NULL DEFAULT 0, - email VARCHAR, - protected SMALLINT NOT NULL DEFAULT 0, + -- is admin user admin SMALLINT NOT NULL DEFAULT 0, + -- last login log last_login TIMESTAMP WITHOUT TIME ZONE, + -- login error login_error_count INT DEFAULT 0, login_error_date_last TIMESTAMP WITHOUT TIME ZONE, login_error_date_first TIMESTAMP WITHOUT TIME ZONE, - strict SMALLINT DEFAULT 0, - locked SMALLINT DEFAULT 0, + -- time locked + lock_until TIMESTAMP WITHOUT TIME ZONE, + lock_after TIMESTAMP WITHOUT TIME ZONE, + -- password change password_change_date TIMESTAMP WITHOUT TIME ZONE, -- only when password is first set or changed password_change_interval INTERVAL, -- null if no change is needed, or d/m/y time interval password_reset_time TIMESTAMP WITHOUT TIME ZONE, -- when the password reset was requested password_reset_uid VARCHAR, -- the uid to access the password reset page + -- _GET login id for direct login + login_user_id VARCHAR, -- the login uid, at least 32 chars + login_user_id_set_date TIMESTAMP WITHOUT TIME ZONE, -- when above uid was set + login_user_id_valid_from TIMESTAMP WITHOUT TIME ZONE, -- if set, from when the above uid is valid + login_user_id_valid_until TIMESTAMP WITHOUT TIME ZONE, -- if set, until when the above uid is valid + login_user_id_revalidate_after INTERVAL DEFAULT '0 days', -- user must login to revalidated login id after set days, 0 for forever + login_user_id_locked SMALLINT DEFAULT 0, -- lock for login user id, but still allow normal login + -- additional ACL json block additional_acl JSONB -- additional ACL as JSON string (can be set by other pages) ) INHERITS (edit_generic) WITHOUT OIDS; +COMMENT ON COLUMN edit_user.username IS 'Login username, must set'; +COMMENT ON COLUMN edit_user.password IS 'Login password, must set'; +COMMENT ON COLUMN edit_user.enabled IS 'Login is enabled (master switch)'; +COMMENT ON COLUMN edit_user.deleted IS 'Login is deleted (master switch), overrides all other'; +COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be locked, default off'; +COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; +COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; +COMMENT ON COLUMN edit_user.debug IS 'Turn debug flag on (legacy)'; +COMMENT ON COLUMN edit_user.db_debug IS 'Turn DB debug flag on (legacy)'; +COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; +COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; +COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; +COMMENT ON COLUMN edit_user.login_error_date_last IS 'Last login error date'; +COMMENT ON COLUMN edit_user.login_error_date_first IS 'First login error date, reset on successfull login'; +COMMENT ON COLUMN edit_user.lock_until IS 'Account is locked until this date, <'; +COMMENT ON COLUMN edit_user.lock_after IS 'Account is locked after this date, >'; +COMMENT ON COLUMN edit_user.password_change_date IS 'Password was changed on'; +COMMENT ON COLUMN edit_user.password_change_interval IS 'After how many days the password has to be changed'; COMMENT ON COLUMN edit_user.password_reset_time IS 'When the password reset was requested. For reset page uid valid check'; -COMMENT ON COLUMN edit_user.password_reset_uid IS 'Password reset page uid'; +COMMENT ON COLUMN edit_user.password_reset_uid IS 'Password reset page uid, one time, invalid after reset successful or time out'; +COMMENT ON COLUMN edit_user.login_user_id IS 'Min 32 character UID to be used to login without password. Via GET/POST parameter'; +COMMENT ON COLUMN edit_user.login_user_id_set_date IS 'login id was set at what date'; +COMMENT ON COLUMN edit_user.login_user_id_valid_from IS 'login id is valid from this date, >='; +COMMENT ON COLUMN edit_user.login_user_id_valid_until IS 'login id is valid until this date, <='; +COMMENT ON COLUMN edit_user.login_user_id_revalidate_after IS 'If set to a number greater 0 then user must login after given amount of days to revalidate, set to 0 for valid forver'; +COMMENT ON COLUMN edit_user.login_user_id_locked IS 'A separte lock flag for login id, user can still login normal'; +COMMENT ON COLUMN edit_user.additional_acl IS 'Additional Access Control List stored in JSON format'; diff --git a/4dev/database/trigger/trg_edit_user.sql b/4dev/database/trigger/trg_edit_user.sql index 1c5bf0dc..ea612d34 100644 --- a/4dev/database/trigger/trg_edit_user.sql +++ b/4dev/database/trigger/trg_edit_user.sql @@ -2,3 +2,8 @@ CREATE TRIGGER trg_edit_user BEFORE INSERT OR UPDATE ON edit_user FOR EACH ROW EXECUTE PROCEDURE set_edit_generic(); + +-- DROP TRIGGER IF EXISTS trg_edit_user_set_login_user_id_set_date ON edit_user; +CREATE TRIGGER trg_edit_user_set_login_user_id_set_date +BEFORE INSERT OR UPDATE ON edit_user +FOR EACH ROW EXECUTE PROCEDURE set_login_user_id_set_date(); diff --git a/4dev/database/update/edit_update_missing_cuid.sql b/4dev/database/update/20190910-edit_update_missing_cuid.sql similarity index 100% rename from 4dev/database/update/edit_update_missing_cuid.sql rename to 4dev/database/update/20190910-edit_update_missing_cuid.sql diff --git a/4dev/database/update/edit_tables_missing_columns.sql b/4dev/database/update/20191211-edit_tables_missing_columns.sql similarity index 100% rename from 4dev/database/update/edit_tables_missing_columns.sql rename to 4dev/database/update/20191211-edit_tables_missing_columns.sql diff --git a/4dev/database/update/20220617-edit_user_login_user_id_add.sql b/4dev/database/update/20220617-edit_user_login_user_id_add.sql new file mode 100644 index 00000000..016835dd --- /dev/null +++ b/4dev/database/update/20220617-edit_user_login_user_id_add.sql @@ -0,0 +1,47 @@ +-- 2022/6/17 update edit_user with login uid + +-- the login uid, at least 32 chars +ALTER TABLE edit_user ADD login_user_id VARCHAR; +-- when above uid was set +ALTER TABLE edit_user ADD login_user_id_set_date TIMESTAMP WITHOUT TIME ZONE; +-- if set, from/until when the above uid is valid +ALTER TABLE edit_user ADD login_user_id_valid_from TIMESTAMP WITHOUT TIME ZONE; +ALTER TABLE edit_user ADD login_user_id_valid_until TIMESTAMP WITHOUT TIME ZONE; +-- user must login to revalidated login id after set days, 0 for forever +ALTER TABLE edit_user ADD login_user_id_revalidate_after INTERVAL NOT NULL DEFAULT '0 days'; +-- lock for login user id, but still allow normal login +ALTER TABLE edit_user ADD login_user_id_locked SMALLINT NOT NULL DEFAULT 0; + +-- disable login before date +ALTER TABLE edit_user ADD lock_until TIMESTAMP WITHOUT TIME ZONE; +-- disable login after date +ALTER TABLE edit_user ADD lock_after TIMESTAMP WITHOUT TIME ZONE; + +CREATE OR REPLACE FUNCTION set_login_user_id_set_date() +RETURNS TRIGGER AS +$$ +BEGIN + -- if new is not null/empty + -- and old one is null or old one different new one + -- set NOW() + -- if new one is NULL + -- set NULL + IF + NEW.login_user_id IS NOT NULL AND NEW.login_user_id <> '' AND + (OLD.login_user_id IS NULL OR NEW.login_user_id <> OLD.login_user_id) + THEN + NEW.login_user_id_set_date = NOW(); + ELSIF NEW.login_user_id IS NULL OR NEW.login_user_id = '' THEN + NEW.login_user_id_set_date = NULL; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; + + +CREATE TRIGGER trg_edit_user_set_login_user_id_set_date +BEFORE INSERT OR UPDATE ON edit_user +FOR EACH ROW EXECUTE PROCEDURE set_login_user_id_set_date(); + +-- __END__ diff --git a/4dev/tests/CoreLibsACLLoginTest.php b/4dev/tests/CoreLibsACLLoginTest.php index 757316d5..0dd5e2ae 100644 --- a/4dev/tests/CoreLibsACLLoginTest.php +++ b/4dev/tests/CoreLibsACLLoginTest.php @@ -16,8 +16,6 @@ final class CoreLibsACLLoginTest extends TestCase { private static $db; private static $log; - /** @var \CoreLibs\Create\Session&MockObject */ - private static $session; /** * start DB conneciton, setup DB, etc @@ -101,6 +99,8 @@ final class CoreLibsACLLoginTest extends TestCase 'Cannot find edit_user table in ACL\Login database for testing' ); } + // always disable max query calls + self::$db->dbSetMaxQueryCall(-1); // insert additional content for testing (locked user, etc) $queries = [ "INSERT INTO edit_access_data " @@ -158,6 +158,7 @@ final class CoreLibsACLLoginTest extends TestCase public function loginProvider(): array { // 0: mock settings/override flag settings + // 2: get array IN // 1: post array IN // login_login, login_username, login_password, login_logout // change_password, pw_username, pw_old_password, pw_new_password, @@ -174,6 +175,7 @@ final class CoreLibsACLLoginTest extends TestCase ], [], [], + [], 3000, [ 'login_error' => 0, @@ -191,6 +193,7 @@ final class CoreLibsACLLoginTest extends TestCase ], [], [], + [], 3000, [ 'login_error' => 0, @@ -213,6 +216,7 @@ final class CoreLibsACLLoginTest extends TestCase ], [], [], + [], 3000, [ 'login_error' => 0, @@ -230,6 +234,7 @@ final class CoreLibsACLLoginTest extends TestCase 'page_name' => 'edit_users.php', ], [], + [], [ 'EUID' => 1, ], @@ -246,6 +251,7 @@ final class CoreLibsACLLoginTest extends TestCase 'page_access' => 'list', ], [], + [], [ 'EUID' => 1, 'USER_NAME' => '', @@ -288,6 +294,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', ], + [], [ 'login_login' => 'Login', 'login_username' => '', @@ -308,6 +315,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', ], + [], [ 'login_login' => 'Login', 'login_username' => '', @@ -328,6 +336,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', ], + [], [ 'login_login' => 'Login', 'login_username' => 'abc', @@ -348,6 +357,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', ], + [], [ 'login_login' => 'Login', 'login_username' => 'abc', @@ -371,6 +381,7 @@ final class CoreLibsACLLoginTest extends TestCase [ 'page_name' => 'edit_users.php', ], + [], [ 'login_login' => 'Login', 'login_username' => 'admin', @@ -387,6 +398,31 @@ final class CoreLibsACLLoginTest extends TestCase . 'Login Failed - Wrong Username or Password' ] ], + // login: ok (but deleted) + 'login: ok, but deleted' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_deleted' => true + ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 106, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User is deleted', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User is deleted' + ] + ], // login: ok (but not enabled) 'login: ok, but not enabled' => [ [ @@ -396,6 +432,7 @@ final class CoreLibsACLLoginTest extends TestCase 'page_access' => 'list', 'test_enabled' => true ], + [], [ 'login_login' => 'Login', 'login_username' => 'admin', @@ -420,6 +457,7 @@ final class CoreLibsACLLoginTest extends TestCase 'page_access' => 'list', 'test_locked' => true ], + [], [ 'login_login' => 'Login', 'login_username' => 'admin', @@ -446,6 +484,7 @@ final class CoreLibsACLLoginTest extends TestCase 'max_login_error_count' => 2, 'test_locked_strict' => true, ], + [], [ 'login_login' => 'Login', 'login_username' => 'admin', @@ -458,6 +497,136 @@ final class CoreLibsACLLoginTest extends TestCase 'login_error' => 105, ] ], + // login ok, but in locked period (until) + 'login: ok, but locked period (until:on)' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_locked_period_until' => 'on' + ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 107, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User in locked via date period', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User in locked via date period' + ] + ], + // login ok, but in locked period (until) + 'login: ok, but locked period (until:off)' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_locked_period_until' => 'off' + ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // login ok, but in locked period (after) + 'login: ok, but locked period (after:on)' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_locked_period_after' => 'on' + ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 107, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User in locked via date period', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User in locked via date period' + ] + ], + // login ok, but in locked period (until, after) + 'login: ok, but locked period (until:on, after:on)' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_locked_period_until' => 'on', + 'test_locked_period_after' => 'on' + ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 107, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User in locked via date period', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User in locked via date period' + ] + ], + // login ok, but login user id locked + 'login: ok, but login user id locked' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_locked' => true + ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 3000, + [ + 'login_error' => 108, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - User is locked via Login User ID', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - User is locked via Login User ID' + ] + ], // login: ok 'login: ok' => [ [ @@ -468,6 +637,148 @@ final class CoreLibsACLLoginTest extends TestCase 'base_access' => 'list', 'page_access' => 'list', ], + [], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // login check via _GET loginUserId + 'login: ok, _GET loginUserId' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // login check via _POST loginUserId + 'login: ok, _POST loginUserId' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // login: wrong GET loginUserId + 'login: ok, illegal chars in _GET loginUserId' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG' + ], + [ + 'loginUserId' => '123$%_/45678¥\-^9~~0$AB&CDEFG', + ], + [], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + 'login: not matching _GET loginUserId' => [ + [ + 'page_name' => 'edit_users.php', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG' + ], + [ + 'loginUserId' => 'ABC' + ], + [], + [], + 3000, + [ + 'login_error' => 1010, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Wrong Username or Password', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Wrong Username or Password' + ] + ], + // login ok with both _GET loginUserId and _POST login username/password + 'login: ok, _GET loginUserId AND login post user data' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], [ 'login_login' => 'Login', 'login_username' => 'admin', @@ -485,9 +796,208 @@ final class CoreLibsACLLoginTest extends TestCase 'page_access' => true, ] ], + // login with invalid loginUserId but valid username/password + 'login: ok, bad _GET loginUserId AND good login post user data' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => 'ABCS', + ], + [ + 'login_login' => 'Login', + 'login_username' => 'admin', + 'login_password' => 'admin', + ], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // loginUserId check with revalidate on/off + 'login: ok, but revalidate trigger, _GET loginUserId' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_revalidate_after' => 'on', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 3000, + [ + 'login_error' => 1101, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Login User ID must be validated', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Login User ID must be validated' + ] + ], + // loginUserId check with revalidate on/off + 'login: ok, revalidate set (outside), _GET loginUserId' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_revalidate_after' => 'off', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // loginUserId check with active time from only + 'login: ok, _GET loginUserId, but outside valid (from:on) ' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_valid_from' => 'on', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 3000, + [ + 'login_error' => 1102, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Login User ID is outside valid date range', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Login User ID is outside valid date range' + ] + ], + // loginUserId check with inactive time from only + 'login: ok, _GET loginUserId, but outside valid (from:off) ' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'edit_access_uid' => 'AdminAccess', + 'edit_access_data' => 'test', + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_valid_from' => 'off', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 0, + [ + 'login_error' => 0, + 'admin_flag' => true, + 'check_access' => true, + 'check_access_id' => 1, + 'check_access_data' => 'value', + 'base_access' => true, + 'page_access' => true, + ] + ], + // loginUserId check with active time until only + 'login: ok, _GET loginUserId, but outside valid (until:on) ' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_valid_until' => 'on', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 3000, + [ + 'login_error' => 1102, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Login User ID is outside valid date range', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Login User ID is outside valid date range' + ] + ], + // loginUserId check with active time from/until + 'login: ok, _GET loginUserId, but outside valid (from:on,until:on) ' => [ + [ + 'page_name' => 'edit_users.php', + 'edit_access_id' => 1, + 'base_access' => 'list', + 'page_access' => 'list', + 'test_login_user_id_valid_from' => 'on', + 'test_login_user_id_valid_until' => 'on', + 'test_login_user_id' => true, + 'test_username' => 'admin', + 'loginUserId' => '1234567890ABCDEFG', + ], + [ + 'loginUserId' => '1234567890ABCDEFG', + ], + [], + [], + 3000, + [ + 'login_error' => 1102, + 'error_string' => 'Fatal Error: ' + . 'Login Failed - Login User ID is outside valid date range', + 'error_string_text' => 'Fatal Error: ' + . 'Login Failed - Login User ID is outside valid date range' + ] + ], // // other: // login check edit access id of ID not null and not in array + // login OK, but during action user gets disabled/deleted/etc ]; } @@ -498,14 +1008,16 @@ final class CoreLibsACLLoginTest extends TestCase * @testdox ACL\Login Class tests [$_dataName] * * @param array $mock_settings + * @param array $get * @param array $post * @param array $session * @param int $error * @param array $expected * @return void */ - public function testACLLogin( + public function testACLLoginFlow( array $mock_settings, + array $get, array $post, array $session, int $error, @@ -534,6 +1046,11 @@ final class CoreLibsACLLoginTest extends TestCase }) ); + // set _GET data + foreach ($get as $get_var => $get_value) { + $_GET[$get_var] = $get_value; + } + // set _POST data foreach ($post as $post_var => $post_value) { $_POST[$post_var] = $post_value; @@ -574,6 +1091,47 @@ final class CoreLibsACLLoginTest extends TestCase . self::$db->dbEscapeLiteral($post['login_username']) ); } + if (!empty($mock_settings['test_deleted'])) { + self::$db->dbExec( + "UPDATE edit_user SET deleted = 1 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + if (!empty($mock_settings['test_login_user_id_locked'])) { + self::$db->dbExec( + "UPDATE edit_user SET login_user_id_locked = 1 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + if ( + !empty($mock_settings['test_locked_period_until']) || + !empty($mock_settings['test_locked_period_after']) + ) { + $q_sub = ''; + if (!empty($mock_settings['test_locked_period_until'])) { + if ($mock_settings['test_locked_period_until'] == 'on') { + $q_sub .= "lock_until = NOW() + '1 day'::interval "; + } elseif ($mock_settings['test_locked_period_until'] == 'off') { + $q_sub .= "lock_until = NOW() - '1 day'::interval "; + } + } + if (!empty($mock_settings['test_locked_period_after'])) { + if (!empty($q_sub)) { + $q_sub .= ", "; + } + if ($mock_settings['test_locked_period_after'] == 'on') { + $q_sub .= "lock_after = NOW() - '1 day'::interval "; + } elseif ($mock_settings['test_locked_period_after'] == 'off') { + $q_sub .= "lock_after = NOW() + '1 day'::interval "; + } + } + self::$db->dbExec( + "UPDATE edit_user SET " + . $q_sub + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } // test locked already if (!empty($mock_settings['test_locked'])) { self::$db->dbExec( @@ -635,6 +1193,62 @@ final class CoreLibsACLLoginTest extends TestCase // set correct password next locked login $_POST['login_password'] = $post['login_password']; } + if (!empty($mock_settings['test_login_user_id'])) { + self::$db->dbExec( + "UPDATE edit_user SET " + . "login_user_id_set_date = NOW(), " + . "login_user_id = " + . self::$db->dbEscapeLiteral($mock_settings['loginUserId']) + . " " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($mock_settings['test_username']) + ); + } + if (!empty($mock_settings['test_login_user_id_revalidate_after'])) { + $q_sub = ''; + if ($mock_settings['test_login_user_id_revalidate_after'] == 'on') { + $q_sub = "login_user_id_set_date = NOW() - '1 day'::interval, " + . "login_user_id_revalidate_after = '1 day'::interval "; + } else { + $q_sub = "login_user_id_set_date = NOW(), " + . "login_user_id_revalidate_after = '6 day'::interval "; + } + self::$db->dbExec( + "UPDATE edit_user SET " + . $q_sub + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($mock_settings['test_username']) + ); + } + if ( + !empty($mock_settings['test_login_user_id_valid_from']) || + !empty($mock_settings['test_login_user_id_valid_until']) + ) { + $q_sub = ''; + if (!empty($mock_settings['test_login_user_id_valid_from'])) { + if ($mock_settings['test_login_user_id_valid_from'] == 'on') { + $q_sub .= "login_user_id_valid_from = NOW() + '1 day'::interval "; + } elseif ($mock_settings['test_login_user_id_valid_from'] == 'off') { + $q_sub .= "login_user_id_valid_from = NOW() - '1 day'::interval "; + } + } + if (!empty($mock_settings['test_login_user_id_valid_until'])) { + if (!empty($q_sub)) { + $q_sub .= ", "; + } + if ($mock_settings['test_login_user_id_valid_until'] == 'on') { + $q_sub .= "login_user_id_valid_until = NOW() - '1 day'::interval "; + } elseif ($mock_settings['test_login_user_id_valid_until'] == 'off') { + $q_sub .= "login_user_id_valid_until = NOW() + '1 day'::interval "; + } + } + self::$db->dbExec( + "UPDATE edit_user SET " + . $q_sub + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($mock_settings['test_username']) + ); + } // run test try { @@ -734,6 +1348,13 @@ final class CoreLibsACLLoginTest extends TestCase $login_mock->loginGetAcl(), 'Assert get acl is array' ); + // if loginUserId in _GET or _POST check that it is set + if (!empty($get['loginUserId']) || !empty($post['loginUserId'])) { + $this->assertNotEmpty( + $login_mock->loginGetLoginUserId(), + 'Assert loginUserId is set' + ); + } // TODO: detail match of ACL array (loginGetAcl) // .. end with: loginLogoutUser @@ -840,6 +1461,35 @@ final class CoreLibsACLLoginTest extends TestCase ); } + // always check, even on error or not set + if (!$login_mock->loginGetLoginUserIdUnclean()) { + $this->assertEquals( + $_GET['loginUserId'] ?? $_POST['loginUserId'] ?? '', + $login_mock->loginGetLoginUserId(), + 'Assert loginUserId matches' + ); + } else { + $this->assertTrue( + $login_mock->loginGetLoginUserIdUnclean(), + 'Assert loginUserId is unclear' + ); + $this->assertNotEquals( + $_GET['loginUserId'] ?? $_POST['loginUserId'] ?? '', + $login_mock->loginGetLoginUserId(), + 'Assert loginUserId does not matche _GET/_POST' + ); + } + // check get/post login user id + $this->assertEquals( + (!empty($_GET['loginUserId']) ? + 'GET' : + (!empty($_POST['loginUserId']) ? + 'POST' : '') + ), + $login_mock->loginGetLoginUserIdSource(), + 'Assert loginUserId source matches' + ); + // enable user again if flag set if (!empty($mock_settings['test_enabled'])) { self::$db->dbExec( @@ -848,6 +1498,30 @@ final class CoreLibsACLLoginTest extends TestCase . self::$db->dbEscapeLiteral($post['login_username']) ); } + if (!empty($mock_settings['test_deleted'])) { + self::$db->dbExec( + "UPDATE edit_user SET deleted = 0 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + if (!empty($mock_settings['test_login_user_id_locked'])) { + self::$db->dbExec( + "UPDATE edit_user SET login_user_id_locked = 0 WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } + if ( + !empty($mock_settings['test_locked_period_until']) || + !empty($mock_settings['test_locked_period_after']) + ) { + self::$db->dbExec( + "UPDATE edit_user SET " + . "lock_until = NULL, " + . "lock_after = NULL " + . "WHERE LOWER(username) = " + . self::$db->dbEscapeLiteral($post['login_username']) + ); + } // reset lock flag if (!empty($mock_settings['test_locked'])) { self::$db->dbExec( @@ -866,6 +1540,36 @@ final class CoreLibsACLLoginTest extends TestCase . self::$db->dbEscapeLiteral($post['login_username']) ); } + // if (!empty($mock_settings['test_login_user_id'])) { + // self::$db->dbExec( + // "UPDATE edit_user SET " + // . "login_user_id = NULL, " + // . "login_user_id_set_date = NULL " + // . "WHERE LOWER(username) = " + // . self::$db->dbEscapeLiteral($mock_settings['test_username']) + // ); + // } + // if (!empty($mock_settings['test_login_user_id_revalidate_after'])) { + // self::$db->dbExec( + // "UPDATE edit_user SET " + // . "login_user_id_set_date = NULL, " + // . "login_user_id_revalidate_after = NULL " + // . "WHERE LOWER(username) = " + // . self::$db->dbEscapeLiteral($mock_settings['test_username']) + // ); + // } + // if ( + // !empty($mock_settings['test_login_user_id_valid_from']) || + // !empty($mock_settings['test_login_user_id_valid_until']) + // ) { + // self::$db->dbExec( + // "UPDATE edit_user SET " + // . "login_user_id_valid_from = NULL, " + // . "login_user_id_valid_until = NULL " + // . "WHERE LOWER(username) = " + // . self::$db->dbEscapeLiteral($mock_settings['test_username']) + // ); + // } } // - loginGetAclList (null, invalid,) @@ -1126,7 +1830,7 @@ final class CoreLibsACLLoginTest extends TestCase * @param string $input * @return void */ - public function testACLLoginGetPasswordLenght(string $input): void + public function testACLLoginGetPasswordLength(string $input): void { $_SESSION = []; // init session (as MOCK) diff --git a/4dev/tests/CoreLibsACLLogin_database_prepare.sh b/4dev/tests/CoreLibsACLLogin_database_prepare.sh index 12b776ad..34b37a7a 100755 --- a/4dev/tests/CoreLibsACLLogin_database_prepare.sh +++ b/4dev/tests/CoreLibsACLLogin_database_prepare.sh @@ -7,6 +7,7 @@ # PARAMETER 2: db user WHO MUST BE ABLE TO CREATE A DATABASE # PARAMETER 3: db name # PARAMETER 4: db host +# PARAMETER 5: print out for testing load_sql="${1}"; # abort with 1 if we cannot find the file @@ -34,8 +35,13 @@ if [ $? -ne 0 ]; then echo 4; exit 4; fi; -# load data (redirect ALL error to null), on error exit with 5 -psql -U ${db_user} -h ${db_host} -f ${load_sql} ${db_name} 2>&1 1>/dev/null 2>/dev/null; +# if error 5 thrown, test with enabled below +if [ ! -z "${5}" ]; then + psql -U ${db_user} -h ${db_host} -f ${load_sql} ${db_name}; +else + # load data (redirect ALL error to null), on error exit with 5 + psql -U ${db_user} -h ${db_host} -f ${load_sql} ${db_name} 2>&1 1>/dev/null 2>/dev/null; +fi; if [ $? -ne 0 ]; then echo 5; exit 5; diff --git a/4dev/tests/database/CoreLibsACLLogin_database_create_data.sql b/4dev/tests/database/CoreLibsACLLogin_database_create_data.sql index 913ba022..d8f4bc61 100644 --- a/4dev/tests/database/CoreLibsACLLogin_database_create_data.sql +++ b/4dev/tests/database/CoreLibsACLLogin_database_create_data.sql @@ -2,7 +2,8 @@ -- create random string with length X CREATE FUNCTION random_string(randomLength int) -RETURNS text AS $$ +RETURNS text AS +$$ SELECT array_to_string( ARRAY( SELECT substring( @@ -14,53 +15,58 @@ SELECT array_to_string( ), '' ) -$$ LANGUAGE SQL +$$ +LANGUAGE SQL RETURNS NULL ON NULL INPUT -VOLATILE; -- LEAKPROOF;-- END: function/random_string.sql +VOLATILE; -- LEAKPROOF; +-- END: function/random_string.sql -- START: function/set_edit_generic.sql -- adds the created or updated date tags -CREATE OR REPLACE FUNCTION set_edit_generic() RETURNS TRIGGER AS ' - DECLARE - random_length INT = 12; -- that should be long enough - BEGIN - IF TG_OP = ''INSERT'' THEN - NEW.date_created := ''now''; - NEW.cuid := random_string(random_length); - ELSIF TG_OP = ''UPDATE'' THEN - NEW.date_updated := ''now''; - END IF; - RETURN NEW; - END; -' LANGUAGE 'plpgsql'; +CREATE OR REPLACE FUNCTION set_edit_generic() +RETURNS TRIGGER AS +$$ +DECLARE + random_length INT = 12; -- that should be long enough +BEGIN + IF TG_OP = 'INSERT' THEN + NEW.date_created := 'now'; + NEW.cuid := random_string(random_length); + ELSIF TG_OP = 'UPDATE' THEN + NEW.date_updated := 'now'; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; -- END: function/set_edit_generic.sql -- START: function/edit_access_set_uid.sql -- add uid add for edit_access table CREATE OR REPLACE FUNCTION set_edit_access_uid() RETURNS TRIGGER AS $$ - DECLARE - myrec RECORD; - v_uid VARCHAR; - BEGIN - -- skip if NEW.name is not set - IF NEW.name IS NOT NULL AND NEW.name <> '' THEN - -- use NEW.name as base, remove all spaces - -- name data is already unique, so we do not need to worry about this here - v_uid := REPLACE(NEW.name, ' ', ''); - IF TG_OP = 'INSERT' THEN - -- always set +DECLARE + myrec RECORD; + v_uid VARCHAR; +BEGIN + -- skip if NEW.name is not set + IF NEW.name IS NOT NULL AND NEW.name <> '' THEN + -- use NEW.name as base, remove all spaces + -- name data is already unique, so we do not need to worry about this here + v_uid := REPLACE(NEW.name, ' ', ''); + IF TG_OP = 'INSERT' THEN + -- always set + NEW.uid := v_uid; + ELSIF TG_OP = 'UPDATE' THEN + -- check if not set, then set + SELECT INTO myrec t.* FROM edit_access t WHERE edit_access_id = NEW.edit_access_id; + IF FOUND THEN NEW.uid := v_uid; - ELSIF TG_OP = 'UPDATE' THEN - -- check if not set, then set - SELECT INTO myrec t.* FROM edit_access t WHERE edit_access_id = NEW.edit_access_id; - IF FOUND THEN - NEW.uid := v_uid; - END IF; END IF; END IF; - RETURN NEW; - END; + END IF; + RETURN NEW; +END; $$ LANGUAGE 'plpgsql'; -- END: function/edit_access_set_uid.sql @@ -69,28 +75,28 @@ $$ CREATE OR REPLACE FUNCTION set_edit_group_uid() RETURNS TRIGGER AS $$ - DECLARE - myrec RECORD; - v_uid VARCHAR; - BEGIN - -- skip if NEW.name is not set - IF NEW.name IS NOT NULL AND NEW.name <> '' THEN - -- use NEW.name as base, remove all spaces - -- name data is already unique, so we do not need to worry about this here - v_uid := REPLACE(NEW.name, ' ', ''); - IF TG_OP = 'INSERT' THEN - -- always set +DECLARE + myrec RECORD; + v_uid VARCHAR; +BEGIN + -- skip if NEW.name is not set + IF NEW.name IS NOT NULL AND NEW.name <> '' THEN + -- use NEW.name as base, remove all spaces + -- name data is already unique, so we do not need to worry about this here + v_uid := REPLACE(NEW.name, ' ', ''); + IF TG_OP = 'INSERT' THEN + -- always set + NEW.uid := v_uid; + ELSIF TG_OP = 'UPDATE' THEN + -- check if not set, then set + SELECT INTO myrec t.* FROM edit_group t WHERE edit_group_id = NEW.edit_group_id; + IF FOUND THEN NEW.uid := v_uid; - ELSIF TG_OP = 'UPDATE' THEN - -- check if not set, then set - SELECT INTO myrec t.* FROM edit_group t WHERE edit_group_id = NEW.edit_group_id; - IF FOUND THEN - NEW.uid := v_uid; - END IF; END IF; END IF; - RETURN NEW; - END; + END IF; + RETURN NEW; +END; $$ LANGUAGE 'plpgsql'; -- END: function/edit_group_set_uid.sql @@ -246,6 +252,32 @@ END $$ LANGUAGE 'plpgsql'; -- END: function/edit_log_partition_insert.sql +-- START: function/edit_user_set_login_user_id_set_date.sql +-- set edit user login_user_id_set_date if login_user_id is set +-- NOW() if not empty + +CREATE OR REPLACE FUNCTION set_login_user_id_set_date() +RETURNS TRIGGER AS +$$ +BEGIN + -- if new is not null/empty + -- and old one is null or old one different new one + -- set NOW() + -- if new one is NULL + -- set NULL + IF + NEW.login_user_id IS NOT NULL AND NEW.login_user_id <> '' AND + (OLD.login_user_id IS NULL OR NEW.login_user_id <> OLD.login_user_id) + THEN + NEW.login_user_id_set_date = NOW(); + ELSIF NEW.login_user_id IS NULL OR NEW.login_user_id = '' THEN + NEW.login_user_id_set_date = NULL; + END IF; + RETURN NEW; +END; +$$ +LANGUAGE 'plpgsql'; +-- END: function/edit_user_set_login_user_id_set_date.sql -- START: table/edit_temp_files.sql -- AUTHOR: Clemens Schwaighofer -- DATE: 2005/07/08 @@ -526,34 +558,80 @@ CREATE TABLE edit_user ( FOREIGN KEY (edit_scheme_id) REFERENCES edit_scheme (edit_scheme_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, edit_access_right_id INT NOT NULL, FOREIGN KEY (edit_access_right_id) REFERENCES edit_access_right (edit_access_right_id) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE, - enabled SMALLINT NOT NULL DEFAULT 0, - deleted SMALLINT NOT NULL DEFAULT 0, + -- username/password username VARCHAR UNIQUE, password VARCHAR, + -- name block first_name VARCHAR, last_name VARCHAR, first_name_furigana VARCHAR, last_name_furigana VARCHAR, + -- email + email VARCHAR, + -- eanbled/deleted flag + enabled SMALLINT NOT NULL DEFAULT 0, + deleted SMALLINT NOT NULL DEFAULT 0, + -- general flags + strict SMALLINT DEFAULT 0, + locked SMALLINT DEFAULT 0, + protected SMALLINT NOT NULL DEFAULT 0, + -- legacy, debug flags debug SMALLINT NOT NULL DEFAULT 0, db_debug SMALLINT NOT NULL DEFAULT 0, - email VARCHAR, - protected SMALLINT NOT NULL DEFAULT 0, + -- is admin user admin SMALLINT NOT NULL DEFAULT 0, + -- last login log last_login TIMESTAMP WITHOUT TIME ZONE, + -- login error login_error_count INT DEFAULT 0, login_error_date_last TIMESTAMP WITHOUT TIME ZONE, login_error_date_first TIMESTAMP WITHOUT TIME ZONE, - strict SMALLINT DEFAULT 0, - locked SMALLINT DEFAULT 0, + -- time locked + lock_until TIMESTAMP WITHOUT TIME ZONE, + lock_after TIMESTAMP WITHOUT TIME ZONE, + -- password change password_change_date TIMESTAMP WITHOUT TIME ZONE, -- only when password is first set or changed password_change_interval INTERVAL, -- null if no change is needed, or d/m/y time interval password_reset_time TIMESTAMP WITHOUT TIME ZONE, -- when the password reset was requested password_reset_uid VARCHAR, -- the uid to access the password reset page + -- _GET login id for direct login + login_user_id VARCHAR, -- the login uid, at least 32 chars + login_user_id_set_date TIMESTAMP WITHOUT TIME ZONE, -- when above uid was set + login_user_id_valid_from TIMESTAMP WITHOUT TIME ZONE, -- if set, from when the above uid is valid + login_user_id_valid_until TIMESTAMP WITHOUT TIME ZONE, -- if set, until when the above uid is valid + login_user_id_revalidate_after INTERVAL DEFAULT '0 days', -- user must login to revalidated login id after set days, 0 for forever + login_user_id_locked SMALLINT DEFAULT 0, -- lock for login user id, but still allow normal login + -- additional ACL json block additional_acl JSONB -- additional ACL as JSON string (can be set by other pages) ) INHERITS (edit_generic) WITHOUT OIDS; +COMMENT ON COLUMN edit_user.username IS 'Login username, must set'; +COMMENT ON COLUMN edit_user.password IS 'Login password, must set'; +COMMENT ON COLUMN edit_user.enabled IS 'Login is enabled (master switch)'; +COMMENT ON COLUMN edit_user.deleted IS 'Login is deleted (master switch), overrides all other'; +COMMENT ON COLUMN edit_user.strict IS 'If too many failed logins user will be locked, default off'; +COMMENT ON COLUMN edit_user.locked IS 'Locked from too many wrong password logins'; +COMMENT ON COLUMN edit_user.protected IS 'User can only be chnaged by admin user'; +COMMENT ON COLUMN edit_user.debug IS 'Turn debug flag on (legacy)'; +COMMENT ON COLUMN edit_user.db_debug IS 'Turn DB debug flag on (legacy)'; +COMMENT ON COLUMN edit_user.admin IS 'If set, this user is SUPER admin'; +COMMENT ON COLUMN edit_user.last_login IS 'Last succesfull login tiemstamp'; +COMMENT ON COLUMN edit_user.login_error_count IS 'Number of failed logins, reset on successful login'; +COMMENT ON COLUMN edit_user.login_error_date_last IS 'Last login error date'; +COMMENT ON COLUMN edit_user.login_error_date_first IS 'First login error date, reset on successfull login'; +COMMENT ON COLUMN edit_user.lock_until IS 'Account is locked until this date, <'; +COMMENT ON COLUMN edit_user.lock_after IS 'Account is locked after this date, >'; +COMMENT ON COLUMN edit_user.password_change_date IS 'Password was changed on'; +COMMENT ON COLUMN edit_user.password_change_interval IS 'After how many days the password has to be changed'; COMMENT ON COLUMN edit_user.password_reset_time IS 'When the password reset was requested. For reset page uid valid check'; -COMMENT ON COLUMN edit_user.password_reset_uid IS 'Password reset page uid'; +COMMENT ON COLUMN edit_user.password_reset_uid IS 'Password reset page uid, one time, invalid after reset successful or time out'; +COMMENT ON COLUMN edit_user.login_user_id IS 'Min 32 character UID to be used to login without password. Via GET/POST parameter'; +COMMENT ON COLUMN edit_user.login_user_id_set_date IS 'login id was set at what date'; +COMMENT ON COLUMN edit_user.login_user_id_valid_from IS 'login id is valid from this date, >='; +COMMENT ON COLUMN edit_user.login_user_id_valid_until IS 'login id is valid until this date, <='; +COMMENT ON COLUMN edit_user.login_user_id_revalidate_after IS 'If set to a number greater 0 then user must login after given amount of days to revalidate, set to 0 for valid forver'; +COMMENT ON COLUMN edit_user.login_user_id_locked IS 'A separte lock flag for login id, user can still login normal'; +COMMENT ON COLUMN edit_user.additional_acl IS 'Additional Access Control List stored in JSON format'; -- END: table/edit_user.sql -- START: table/edit_log.sql -- AUTHOR: Clemens Schwaighofer @@ -774,6 +852,11 @@ FOR EACH ROW EXECUTE PROCEDURE set_edit_generic(); CREATE TRIGGER trg_edit_user BEFORE INSERT OR UPDATE ON edit_user FOR EACH ROW EXECUTE PROCEDURE set_edit_generic(); + +-- DROP TRIGGER IF EXISTS trg_edit_user_set_login_user_id_set_date ON edit_user; +CREATE TRIGGER trg_edit_user_set_login_user_id_set_date +BEFORE INSERT OR UPDATE ON edit_user +FOR EACH ROW EXECUTE PROCEDURE set_login_user_id_set_date(); -- END: trigger/trg_edit_user.sql -- START: trigger/trg_edit_visible_group.sql -- DROP TRIGGER IF EXISTS trg_edit_visible_group ON edit_visible_group; diff --git a/www/admin/edit_groups_test.php b/www/admin/edit_groups_test.php new file mode 100644 index 00000000..d9465fe0 --- /dev/null +++ b/www/admin/edit_groups_test.php @@ -0,0 +1,61 @@ + BASE . LOG, + 'file_id' => $LOG_FILE_ID, + // add file date + 'print_file_date' => true, + // set debug and print flags + 'debug_all' => $DEBUG_ALL ?? false, + 'echo_all' => $ECHO_ALL ?? false, + 'print_all' => $PRINT_ALL ?? false, +]); +$db = new CoreLibs\DB\IO(DB_CONFIG, $log); +$login = new CoreLibs\ACL\Login($db, $log, $session); +$locale = \CoreLibs\Language\GetLocale::setLocale(); +$l10n = new \CoreLibs\Language\L10n( + $locale['locale'], + $locale['domain'], + $locale['path'], +); + +print ""; +print "GROUP TESTER"; +print ""; + +print '
'; +print 'Logout'; +print ''; +print '
'; + +print "

TEST Login

"; + +print ""; + +// __END__ diff --git a/www/lib/CoreLibs/ACL/Login.php b/www/lib/CoreLibs/ACL/Login.php index 4e1f455a..4231dd65 100644 --- a/www/lib/CoreLibs/ACL/Login.php +++ b/www/lib/CoreLibs/ACL/Login.php @@ -72,30 +72,36 @@ use CoreLibs\Check\Password; class Login { - /** @var string */ - private $euid; // the user id var + /** @var string the user id var*/ + private $euid; + /** @var string _GET/_POST loginUserId parameter for non password login */ + private $login_user_id; + /** @var string source, either _GET or _POST or empty */ + private $login_user_id_source; + /** @var bool set to true if illegal characters where found in the login user id string */ + private $login_unclear = false; // is set to one if login okay, or EUID is set and user is okay to access this page /** @var bool */ private $permission_okay = false; - /** @var string */ - public $login; // pressed login - /** @var string */ - private $action; // master action command - /** @var string */ - private $username; // login name - /** @var string */ - private $password; // login password - /** @var string */ - private $logout; // logout button - /** @var bool */ - private $password_change = false; // if this is set to true, the user can change passwords - /** @var bool */ - private $password_change_ok = false; // password change was successful + /** @var string pressed login */ + public $login; + /** @var string master action command */ + private $action; + /** @var string login name */ + private $username; + /** @var string login password */ + private $password; + /** @var string logout button */ + private $logout; + /** @var bool if this is set to true, the user can change passwords */ + private $password_change = false; + /** @var bool password change was successful */ + private $password_change_ok = false; // can we reset password and mail to user with new password set screen /** @var bool */ private $password_forgot = false; - /** @var bool */ - // private $password_forgot_ok = false; // password forgot mail send ok + /** @var bool password forgot mail send ok */ + // private $password_forgot_ok = false; /** @var string */ private $change_password; /** @var string */ @@ -106,8 +112,8 @@ class Login private $pw_new_password; /** @var string */ private $pw_new_password_confirm; - /** @var array */ - private $pw_change_deny_users = []; // array of users for which the password change is forbidden + /** @var array array of users for which the password change is forbidden */ + private $pw_change_deny_users = []; /** @var string */ private $logout_target = ''; /** @var int */ @@ -117,8 +123,7 @@ class Login /** @var string */ private $page_name = ''; - // if we have password change we need to define some rules - /** @var int */ + /** @var int if we have password change we need to define some rules */ private $password_min_length = 9; /** @var int an true maxium min, can never be set below this */ private $password_min_length_max = 9; @@ -126,8 +131,7 @@ class Login // it will be set back to 255 /** @var int */ private $password_max_length = 255; - // can have several regexes, if nothing set, all is ok - /** @var array */ + /** @var array can have several regexes, if nothing set, all is ok */ private $password_valid_chars = [ // '^(?=.*\d)(?=.*[A-Za-z])[0-9A-Za-z!@#$%]{8,}$', // '^(?.*(\pL)u)(?=.*(\pN)u)(?=.*([^\pL\pN])u).{8,}', @@ -234,6 +238,14 @@ class Login 'msg' => 'Login Failed - Wrong Username or Password', 'flag' => 'e' ], + '1101' => [ + 'msg' => 'Login Failed - Login User ID must be validated', + 'flag' => 'e' + ], + '1102' => [ + 'msg' => 'Login Failed - Login User ID is outside valid date range', + 'flag' => 'e' + ], '102' => [ 'msg' => 'Login Failed - Please enter username and password', 'flag' => 'e' @@ -250,6 +262,18 @@ class Login 'msg' => 'Login Failed - User is locked', 'flag' => 'e' ], + '106' => [ + 'msg' => 'Login Failed - User is deleted', + 'flag' => 'e' + ], + '107' => [ + 'msg' => 'Login Failed - User in locked via date period', + 'flag' => 'e' + ], + '108' => [ + 'msg' => 'Login Failed - User is locked via Login User ID', + 'flag' => 'e' + ], '109' => [ 'msg' => 'Check permission query reading failed', 'flag' => 'e' @@ -360,6 +384,47 @@ class Login // **** PRIVATE INTERNAL // ************************************************************************* + /** + * Checks for all flags and sets error codes for each + * In order: + * delete > enable > lock > period lock > login user id lock + * + * @param int $deleted User deleted check + * @param int $enabled User not enabled check + * @param int $locked Locked because of too many invalid passwords + * @param int $locked_period Locked because of time period set + * @param int $login_user_id_locked Locked from using Login User Id + * @return bool + */ + private function loginValidationCheck( + int $deleted, + int $enabled, + int $locked, + int $locked_period, + int $login_user_id_locked + ): bool { + $validation = false; + if ($deleted) { + // user is deleted + $this->login_error = 106; + } elseif (!$enabled) { + // user is not enabled + $this->login_error = 104; + } elseif ($locked) { + // user is locked, either set or auto set + $this->login_error = 105; + } elseif ($locked_period) { + // locked date trigger + $this->login_error = 107; + } elseif ($login_user_id_locked) { + // user is locked, either set or auto set + $this->login_error = 108; + } else { + $validation = true; + } + return $validation; + } + /** * checks if password is valid, sets internal error login variable * @@ -393,7 +458,6 @@ class Login ) { // this means password cannot be decrypted because of missing crypt methods $this->login_error = 9999; - $password_ok = false; } elseif ( preg_match("/^\\$2y\\$/", $hash) && !Password::passwordVerify($password, $hash) @@ -401,7 +465,6 @@ class Login // this is the new password hash method, is only $2y$ // all others are not valid anymore $this->login_error = 1013; - $password_ok = false; } elseif ( !preg_match("/^\\$2(a|y)\\$/", $hash) && !preg_match("/^\\$1\\$/", $hash) && @@ -410,7 +473,6 @@ class Login ) { // check old plain password, case sensitive $this->login_error = 1012; - $password_ok = false; } else { // all ok $password_ok = true; @@ -418,6 +480,28 @@ class Login return $password_ok; } + /** + * Check if Login User ID is allowed to login + * + * @param int $login_user_id_valid_date + * @param int $login_user_id_revalidate + * @return bool + */ + private function loginLoginUserIdCheck( + int $login_user_id_valid_date, + int $login_user_id_revalidate + ): bool { + $login_id_ok = false; + if ($login_user_id_revalidate) { + $this->login_error = 1101; + } elseif (!$login_user_id_valid_date) { + $this->login_error = 1102; + } else { + $login_id_ok = true; + } + return $login_id_ok; + } + /** * if user pressed login button this script is called, * but only if there is no preview euid set @@ -427,11 +511,12 @@ class Login private function loginLoginUser(): void { // if pressed login at least and is not yet loggined in - if ($this->euid || !$this->login) { + if ($this->euid || (!$this->login && !$this->login_user_id)) { return; } // if not username AND password where given - if (!($this->password && $this->username)) { + // OR no login_user_id + if (!($this->username && $this->password) && !$this->login_user_id) { $this->login_error = 102; $this->permission_okay = false; return; @@ -441,12 +526,39 @@ class Login $q = "SELECT eu.edit_user_id, eu.username, eu.password, " . "eu.edit_group_id, " . "eg.name AS edit_group_name, admin, " + // login error + locked . "eu.login_error_count, eu.login_error_date_last, " . "eu.login_error_date_first, eu.strict, eu.locked, " + // date based lock + . "CASE WHEN (" + . "(eu.lock_until IS NULL " + . "OR (eu.lock_until IS NOT NULL AND NOW() >= eu.lock_until)) " + . "AND (eu.lock_after IS NULL " + . "OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after))" + . ") THEN 0::INT ELSE 1::INT END locked_period, " + // debug (legacy) . "eu.debug, eu.db_debug, " + // enabled + . "eu.enabled, eu.deleted, " + // login id validation + . "CASE WHEN (" + . "(eu.login_user_id_valid_from IS NULL " + . "OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from)) " + . "AND (eu.login_user_id_valid_until IS NULL " + . "OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until))" + . ") THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, " + // check if user must login + . "CASE WHEN eu.login_user_id_revalidate_after > '0 days'::INTERVAL " + . "AND (eu.login_user_id_set_date + eu.login_user_id_revalidate_after)::DATE " + . "<= NOW()::DATE " + . "THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, " + . "eu.login_user_id_locked, " + // language + . "el.short_name AS locale, el.iso_name AS encoding, " + // levels . "eareu.level AS user_level, eareu.type AS user_type, " . "eareg.level AS group_level, eareg.type AS group_type, " - . "eu.enabled, el.short_name AS locale, el.iso_name AS encoding, " + // colors . "first.header_color AS first_header_color, " . "second.header_color AS second_header_color, second.template " . "FROM edit_user eu " @@ -458,11 +570,17 @@ class Login . "edit_scheme first " . "WHERE first.edit_scheme_id = eg.edit_scheme_id " . "AND eu.edit_group_id = eg.edit_group_id " - . "AND eu.edit_language_id = el.edit_language_id AND " - . "eu.edit_access_right_id = eareu.edit_access_right_id AND " - . "eg.edit_access_right_id = eareg.edit_access_right_id AND " - // password match is done in script, against old plain or new blowfish encypted - . "(LOWER(username) = '" . $this->db->dbEscapeString(strtolower($this->username)) . "') "; + . "AND eu.edit_language_id = el.edit_language_id " + . "AND eu.edit_access_right_id = eareu.edit_access_right_id " + . "AND eg.edit_access_right_id = eareg.edit_access_right_id " + . "AND " + // either login_user_id OR password must be given + . (!empty($this->login_user_id && empty($this->username)) ? + // check with login id if set and NO username + "eu.login_user_id = " . $this->db->dbEscapeLiteral($this->login_user_id) . " " : + // password match is done in script, against old plain or new blowfish encypted + "LOWER(username) = " . $this->db->dbEscapeLiteral(strtolower($this->username)) . " " + ); // reset any query data that might exist $this->db->dbCacheReset($q); // never cache return data @@ -488,17 +606,34 @@ class Login // - password is readable // - encrypted password matches // - plain password matches - - if (!$res['enabled']) { - // user is enabled - $this->login_error = 104; - } elseif ($res['locked']) { - // user is locked, either set or auto set - $this->login_error = 105; - } elseif (!$this->loginPasswordCheck($res['password'])) { + if ( + !$this->loginValidationCheck( + (int)$res['deleted'], + (int)$res['enabled'], + (int)$res['locked'], + (int)$res['locked_period'], + (int)$res['login_user_id_locked'] + ) + ) { + // error set in method (104, 105, 106, 107, 108) + } elseif ( + empty($this->username) && + !empty($this->login_user_id) && + !$this->loginLoginUserIdCheck( + (int)$res['login_user_id_valid_date'], + (int)$res['login_user_id_revalidate'] + ) + ) { + // check done in loginLoginIdCheck method + // aborts on must revalidate and not valid (date range) + } elseif ( + !empty($this->username) && + !$this->loginPasswordCheck($res['password']) + ) { // none to be set, set in login password check // this is not valid password input error here // all error codes are set in loginPasswordCheck method + // also valid if login_user_id is ok } else { // check if the current password is an invalid hash and do a rehash and set password // $this->debug('LOGIN', 'Hash: '.$res['password'].' -> VERIFY: ' @@ -1396,6 +1531,34 @@ EOM; $this->login_is_ajax_page = true; } + // attach outside uid for login post > get > empty + $this->login_user_id = $_POST['loginUserId'] ?? $_GET['loginUserId'] ?? ''; + // cleanup only alphanumeric + if (!empty($this->login_user_id)) { + // set post/get only if actually set + if (isset($_POST['loginUserId'])) { + $this->login_user_id_source = 'POST'; + } elseif (isset($_GET['loginUserId'])) { + $this->login_user_id_source = 'GET'; + } + // clean login user id + $login_user_id_changed = 0; + $this->login_user_id = preg_replace( + "/[^A-Za-z0-9]/", + '', + $this->login_user_id, + -1, + $login_user_id_changed + ); + // flag unclean input data + if ($login_user_id_changed > 0) { + $this->login_unclear = true; + // error for invalid user id? + $this->log->debug('LOGIN USER ID', 'Invalid characters: ' + . $login_user_id_changed . ' in loginUserId: ' + . $this->login_user_id . ' (' . $this->login_user_id_source . ')'); + } + } // if there is none, there is none, saves me POST/GET check $this->euid = array_key_exists('EUID', $_SESSION) ? $_SESSION['EUID'] : 0; // get login vars, are so, can't be changed @@ -1706,8 +1869,30 @@ EOM; if ($this->login_error == 103) { return $this->permission_okay; } - // if ($this->euid && $this->login_error != 103) { - $q = "SELECT ep.filename " + $q = "SELECT ep.filename, " + // base lock flags + . "eu.deleted, eu.enabled, eu.locked, " + // date based lock + . "CASE WHEN (" + . "(eu.lock_until IS NULL " + . "OR (eu.lock_until IS NOT NULL AND NOW() >= eu.lock_until)) " + . "AND (eu.lock_after IS NULL " + . "OR (eu.lock_after IS NOT NULL AND NOW() <= eu.lock_after))" + . ") THEN 0::INT ELSE 1::INT END locked_period, " + // login id validation + . "login_user_id, " + . "CASE WHEN (" + . "(eu.login_user_id_valid_from IS NULL " + . "OR (eu.login_user_id_valid_from IS NOT NULL AND NOW() >= eu.login_user_id_valid_from)) " + . "AND (eu.login_user_id_valid_until IS NULL " + . "OR (eu.login_user_id_valid_until IS NOT NULL AND NOW() <= eu.login_user_id_valid_until))" + . ") THEN 1::INT ELSE 0::INT END AS login_user_id_valid_date, " + // check if user must login + . "CASE WHEN eu.login_user_id_revalidate_after > '0 days'::INTERVAL " + . "AND eu.login_user_id_set_date + eu.login_user_id_revalidate_after <= NOW()::DATE " + . "THEN 1::INT ELSE 0::INT END AS login_user_id_revalidate, " + . "eu.login_user_id_locked " + // . "FROM edit_page ep, edit_page_access epa, edit_group eg, edit_user eu " . "WHERE ep.edit_page_id = epa.edit_page_id " . "AND eg.edit_group_id = epa.edit_group_id " @@ -1720,6 +1905,30 @@ EOM; $this->login_error = 109; return $this->permission_okay; } + if ( + !$this->loginValidationCheck( + (int)$res['deleted'], + (int)$res['enabled'], + (int)$res['locked'], + (int)$res['locked_period'], + (int)$res['login_user_id_locked'] + ) + ) { + // errors set in method + return $this->permission_okay; + } + // if login user id parameter and no username, check period here + if ( + empty($this->username) && + !empty($this->login_user_id) && + !$this->loginLoginUserIdCheck( + (int)$res['login_user_id_valid_date'], + (int)$res['login_user_id_revalidate'] + ) + ) { + // errors set in method + return $this->permission_okay; + } if (isset($res['filename']) && $res['filename'] == $this->page_name) { $this->permission_okay = true; } else { @@ -1917,6 +2126,37 @@ EOM; return false; } + /** + * Returns current set loginUserId or empty if unset + * + * @return string loginUserId or empty string for not set + */ + public function loginGetLoginUserId(): string + { + return $this->login_user_id; + } + + /** + * Returns GET/POST for where the loginUserId was set + * + * @return string GET or POST or empty string for not set + */ + public function loginGetLoginUserIdSource(): string + { + return $this->login_user_id_source; + } + + /** + * Returns unclear login user id state. If true then illegal characters + * where present in the loginUserId parameter + * + * @return bool False for clear, True if illegal characters found + */ + public function loginGetLoginUserIdUnclean(): bool + { + return $this->login_unclear; + } + /** * old name for loginGetEditAccessData *