From 58f1c0b03e6deb3bd3b0ea4fe9b1028bd57236a5 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 14:05:37 +0200 Subject: [PATCH 01/61] Add duplicate unit tests to cover create_user with the UUID user ID as a start. Bail out in create_user if UUID format is chosen and user doesn't have the corresponding unique_id field. --- tests/test_mig_shared_useradm.py | 223 ++++++++++++++++++++++++++++++- 1 file changed, 221 insertions(+), 2 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 5108d4cb2..8d13042e8 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -38,7 +38,7 @@ # Imports required for the unit test wrapping from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( - DEFAULT_USER_ID_FORMAT, + DEFAULT_USER_ID_FORMAT, UUID_USER_ID_FORMAT, htaccess_filename, keyword_auto, ssh_conf_dir, @@ -148,6 +148,7 @@ class TestMigSharedUseradm__create_user( def before_each(self): configuration = self.configuration + configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT _ensure_dirs_needed_for_userdb(self.configuration) @@ -424,6 +425,225 @@ def test_user_creation_with_id_collission_fails(self): ) +# TODO: consider merging the tests for UUID and X509 format +class TestMigSharedUseradm__create_user_uuid_user_id( + MigTestCase, FixtureAssertMixin, PickleAssertMixin +): + """Coverage of useradm create_user function with UUID format.""" + + TEST_USER_DN_GDP = "%s/GDP" % (TEST_USER_DN,) + TEST_USER_PASSWORD_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM2134uix" + + def before_each(self): + configuration = self.configuration + configuration.site_user_id_format = UUID_USER_ID_FORMAT + + _ensure_dirs_needed_for_userdb(self.configuration) + + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def _provide_configuration(self): + return "testconfig" + + def test_user_db_is_created(self): + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "user@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["password"] = "password" + create_user( + user_dict, self.configuration, keyword_auto, default_renew=True + ) + + # presence of user home + path_kind = MigTestCase._absolute_path_kind(self.expected_user_db_home) + self.assertEqual(path_kind, "dir") + + # presence of user db + path_kind = MigTestCase._absolute_path_kind(self.expected_user_db_file) + self.assertEqual(path_kind, "file") + + def test_user_creation_records_a_user(self): + def _adjust_user_dict_for_compare(user_obj): + obj = dict(user_obj) + obj["created"] = 9999999999.9999999 + obj["expire"] = 9999999999.9999999 + obj["unique_id"] = "__UNIQUE_ID__" + return obj + + expected_user_id = TEST_USER_DN + expected_user_password_hash = self.TEST_USER_PASSWORD_HASH + + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = expected_user_password_hash + + create_user( + user_dict, self.configuration, keyword_auto, default_renew=True + ) + + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + self.assertIn(expected_user_id, pickled) + + prepared = self.prepareFixtureAssert( + "MiG-users.db--example", fixture_format="json" + ) + + # TODO: remove resetting the handful of keys here + # this is done to allow the comparision to succeed + actual_user_object = _adjust_user_dict_for_compare( + pickled[expected_user_id] + ) + expected_user_object = _adjust_user_dict_for_compare( + prepared.fixture_data[expected_user_id] + ) + + self.maxDiff = None + self.assertEqual(actual_user_object, expected_user_object) + + def test_user_creation_records_a_user_with_gdp(self): + self.configuration.site_enable_gdp = True + + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + # explicitly setting set a DN suffixed user DN to force GDP + user_dict["distinguished_name"] = self.TEST_USER_DN_GDP + + try: + create_user( + user_dict, self.configuration, keyword_auto, default_renew=True + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_creation_and_renew_records_a_user(self): + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_creation_fails_in_renew_when_locked(self): + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + # explicitly setting set a DN suffixed user DN to force GDP + user_dict["distinguished_name"] = self.TEST_USER_DN_GDP + user_dict["status"] = "locked" + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_creation_with_id_collission_fails(self): + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "user@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["password"] = "password" + user_dict["distinguished_name"] = TEST_USER_DN + + try: + create_user( + user_dict, self.configuration, keyword_auto, default_renew=True + ) + except: + self.assertFalse(True, "should not be reached") + + # NOTE: reset distinguished_name and introduce an ID conflict to test + del user_dict["distinguished_name"] + user_dict["organization"] = "Another Org" + with self.assertRaises(Exception): + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + + class MigSharedUseradm__assure_current_htaccess(MigTestCase): """Coverage of useradm behaviours around htaccess.""" @@ -1092,6 +1312,5 @@ def test_get_any_oid_user_dn_user_check_false(self): ) self.assertEqual(result, raw_login) - if __name__ == "__main__": testmain() From 26f62294e6c5ab440a84d9cac8a7401cb2217d5f Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 14:38:24 +0200 Subject: [PATCH 02/61] Add unit test to verify expected FS paths are created for UUID users. --- tests/test_mig_shared_useradm.py | 60 ++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 8d13042e8..e628f93d6 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -523,6 +523,66 @@ def _adjust_user_dict_for_compare(user_obj): self.maxDiff = None self.assertEqual(actual_user_object, expected_user_object) + def test_user_creation_creates_fs_entries(self): + user_dict = {} + user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + + create_user( + user_dict, self.configuration, keyword_auto, default_renew=True + ) + home_dir = os.path.join(self.configuration.user_home, + "UniqueUserIdForTestUser") + self.assertTrue(os.path.isdir(home_dir)) + home_link = os.path.join(self.configuration.user_home, + TEST_USER_DIR) + self.assertTrue(os.path.islink(home_link)) + self.assertEqual(os.path.realpath(home_dir), + os.path.realpath(home_link)) + + settings_dir = os.path.join(self.configuration.user_settings, + "UniqueUserIdForTestUser") + self.assertTrue(os.path.isdir(settings_dir)) + settings_link = os.path.join(self.configuration.user_settings, + TEST_USER_DIR) + self.assertTrue(os.path.islink(settings_link)) + self.assertEqual(os.path.realpath(settings_dir), + os.path.realpath(settings_link)) + + ssh_dir = os.path.join(home_dir, ssh_conf_dir) + self.assertTrue(os.path.isdir(ssh_dir)) + davs_dir = os.path.join(home_dir, davs_conf_dir) + self.assertTrue(os.path.isdir(davs_dir)) + ftps_dir = os.path.join(home_dir, ftps_conf_dir) + self.assertTrue(os.path.isdir(ftps_dir)) + htaccess_path = os.path.join(home_dir, htaccess_filename) + self.assertTrue(os.path.isfile(htaccess_path)) + # TODO: test contents matches access for UUID and X509 ID + + welcome_path = os.path.join(home_dir, welcome_filename) + self.assertTrue(os.path.isfile(welcome_path)) + settings_path = os.path.join(settings_dir, settings_filename) + self.assertTrue(os.path.isfile(settings_path)) + profile_path = os.path.join(settings_dir, profile_filename) + self.assertTrue(os.path.isfile(profile_path)) + widgets_path = os.path.join(settings_dir, widgets_filename) + self.assertTrue(os.path.isfile(widgets_path)) + css_path = os.path.join(home_dir, default_css_filename) + self.assertTrue(os.path.isfile(css_path)) + + # TODO: check permissions on htaccess, .ssh + + # TODO: check oid symlinks + def test_user_creation_records_a_user_with_gdp(self): self.configuration.site_enable_gdp = True From 1c6961d60dfbe4d72781e2124cdda3b7568dd9b7 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 16:15:33 +0200 Subject: [PATCH 03/61] Extend unit tests of expected FS contents after user creation both with X509 and UUID format. Reuse constants a bit more. --- tests/test_mig_shared_useradm.py | 48 ++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index e628f93d6..72f1a2da5 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -83,6 +83,7 @@ TEST_USER_DIR = TEST_USER_DN.replace('/', '+').replace(' ', '_') TEST_USER_SHORT_ID = "abc123@some.org" +TEST_USER_UUID = "UniqueUserIdForTestUser" TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] TEST_USER_EXPIRE = 1776031200 OTHER_USER_EMAIL = OTHER_USER_DN.split("/emailAddress=", 1)[-1] @@ -453,7 +454,7 @@ def _provide_configuration(self): def test_user_db_is_created(self): user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -485,7 +486,7 @@ def _adjust_user_dict_for_compare(user_obj): expected_user_password_hash = self.TEST_USER_PASSWORD_HASH user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -525,7 +526,7 @@ def _adjust_user_dict_for_compare(user_obj): def test_user_creation_creates_fs_entries(self): user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -540,8 +541,7 @@ def test_user_creation_creates_fs_entries(self): create_user( user_dict, self.configuration, keyword_auto, default_renew=True ) - home_dir = os.path.join(self.configuration.user_home, - "UniqueUserIdForTestUser") + home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) self.assertTrue(os.path.isdir(home_dir)) home_link = os.path.join(self.configuration.user_home, TEST_USER_DIR) @@ -550,7 +550,7 @@ def test_user_creation_creates_fs_entries(self): os.path.realpath(home_link)) settings_dir = os.path.join(self.configuration.user_settings, - "UniqueUserIdForTestUser") + TEST_USER_UUID) self.assertTrue(os.path.isdir(settings_dir)) settings_link = os.path.join(self.configuration.user_settings, TEST_USER_DIR) @@ -566,28 +566,48 @@ def test_user_creation_creates_fs_entries(self): self.assertTrue(os.path.isdir(ftps_dir)) htaccess_path = os.path.join(home_dir, htaccess_filename) self.assertTrue(os.path.isfile(htaccess_path)) - # TODO: test contents matches access for UUID and X509 ID + # NOTE: test htaccess contents matches access for UUID and X509 ID + req_pattern = 'require user "%s"' + with open(htaccess_path) as test_fd: + test_contents = test_fd.read() + self.assertIn(req_pattern % TEST_USER_DN, test_contents) + # TODO: add UUID to htaccess and enable next? + # self.assertIn(req_pattern % TEST_USER_UUID, htaccess_contents) welcome_path = os.path.join(home_dir, welcome_filename) self.assertTrue(os.path.isfile(welcome_path)) settings_path = os.path.join(settings_dir, settings_filename) self.assertTrue(os.path.isfile(settings_path)) + pickled = self.assertPickledFile(settings_path) + self.assertIn(TEST_USER_DN.encode('utf8'), + pickled['CREATOR'.encode('utf8')]) profile_path = os.path.join(settings_dir, profile_filename) self.assertTrue(os.path.isfile(profile_path)) + pickled = self.assertPickledFile(profile_path) + self.assertIn(TEST_USER_DN.encode('utf8'), + pickled['CREATOR'.encode('utf8')]) widgets_path = os.path.join(settings_dir, widgets_filename) self.assertTrue(os.path.isfile(widgets_path)) + pickled = self.assertPickledFile(widgets_path) + self.assertIn(TEST_USER_DN.encode('utf8'), + pickled['CREATOR'.encode('utf8')]) css_path = os.path.join(home_dir, default_css_filename) self.assertTrue(os.path.isfile(css_path)) + with open(css_path) as test_fd: + test_contents = test_fd.read() + self.assertIn('No changes - use default', test_contents) - # TODO: check permissions on htaccess, .ssh - - # TODO: check oid symlinks + # NOTE: check permissions on htaccess, .ssh + htaccess_stat = os.stat(htaccess_path) + self.assertEqual(htaccess_stat.st_mode, 0o100444) + ssh_stat = os.stat(ssh_dir) + self.assertEqual(ssh_stat.st_mode, 0o40755) def test_user_creation_records_a_user_with_gdp(self): self.configuration.site_enable_gdp = True user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -610,7 +630,7 @@ def test_user_creation_records_a_user_with_gdp(self): def test_user_creation_and_renew_records_a_user(self): user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -646,7 +666,7 @@ def test_user_creation_and_renew_records_a_user(self): def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -674,7 +694,7 @@ def test_user_creation_fails_in_renew_when_locked(self): def test_user_creation_with_id_collission_fails(self): user_dict = {} - user_dict["unique_id"] = "UniqueUserIdForTestUser" + user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" From ae021390f56861241ef797d54af67e14ce8cbb2e Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 16:28:49 +0200 Subject: [PATCH 04/61] Add short_id and verification of corresponding link in FS from create user. --- tests/test_mig_shared_useradm.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 72f1a2da5..a1fc2f57b 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -84,6 +84,7 @@ TEST_USER_SHORT_ID = "abc123@some.org" TEST_USER_UUID = "UniqueUserIdForTestUser" +TEST_USER_SHORT_ID = "abc123@some.org" TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] TEST_USER_EXPIRE = 1776031200 OTHER_USER_EMAIL = OTHER_USER_DN.split("/emailAddress=", 1)[-1] @@ -527,6 +528,7 @@ def _adjust_user_dict_for_compare(user_obj): def test_user_creation_creates_fs_entries(self): user_dict = {} user_dict["unique_id"] = TEST_USER_UUID + user_dict["short_id"] = TEST_USER_SHORT_ID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" @@ -548,6 +550,11 @@ def test_user_creation_creates_fs_entries(self): self.assertTrue(os.path.islink(home_link)) self.assertEqual(os.path.realpath(home_dir), os.path.realpath(home_link)) + short_link = os.path.join(self.configuration.user_home, + TEST_USER_SHORT_ID) + self.assertTrue(os.path.islink(short_link)) + self.assertEqual(os.path.realpath(home_dir), + os.path.realpath(short_link)) settings_dir = os.path.join(self.configuration.user_settings, TEST_USER_UUID) From 159193888ba55c56776fde95518204d0f20f6a7e Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 16:45:30 +0200 Subject: [PATCH 05/61] Add delete user test and verify that it cleans up in FS, too. --- tests/test_mig_shared_useradm.py | 220 ++++++++++++++++++++++++++++++- 1 file changed, 214 insertions(+), 6 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index a1fc2f57b..668886da2 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -369,6 +369,110 @@ def test_user_creation_and_renew_records_a_user(self): except Exception: self.assertFalse(True, "should not be reached") + def test_user_creation_and_delete_records_a_user(self): + user_dict = {} + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + + try: + delete_user( + user_dict, + self.configuration, + keyword_auto, + force=True, + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_deletion_removes_fs_entries(self): + user_dict = {} + user_dict["unique_id"] = TEST_USER_UUID + user_dict["short_id"] = TEST_USER_SHORT_ID + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + try: + delete_user( + user_dict, + self.configuration, + keyword_auto, + force=True, + ) + except: + self.assertFalse(True, "should not be reached") + + home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) + self.assertFalse(os.path.isdir(home_dir)) + home_link = os.path.join(self.configuration.user_home, + TEST_USER_DIR) + self.assertFalse(os.path.islink(home_link)) + short_link = os.path.join(self.configuration.user_home, + TEST_USER_SHORT_ID) + self.assertFalse(os.path.islink(short_link)) + + settings_dir = os.path.join(self.configuration.user_settings, + TEST_USER_UUID) + self.assertFalse(os.path.isdir(settings_dir)) + settings_link = os.path.join(self.configuration.user_settings, + TEST_USER_DIR) + self.assertFalse(os.path.islink(settings_link)) + + ssh_dir = os.path.join(home_dir, ssh_conf_dir) + self.assertFalse(os.path.isdir(ssh_dir)) + davs_dir = os.path.join(home_dir, davs_conf_dir) + self.assertFalse(os.path.isdir(davs_dir)) + ftps_dir = os.path.join(home_dir, ftps_conf_dir) + self.assertFalse(os.path.isdir(ftps_dir)) + htaccess_path = os.path.join(home_dir, htaccess_filename) + self.assertFalse(os.path.isfile(htaccess_path)) + welcome_path = os.path.join(home_dir, welcome_filename) + self.assertFalse(os.path.isfile(welcome_path)) + settings_path = os.path.join(settings_dir, settings_filename) + self.assertFalse(os.path.isfile(settings_path)) + profile_path = os.path.join(settings_dir, profile_filename) + self.assertFalse(os.path.isfile(profile_path)) + widgets_path = os.path.join(settings_dir, widgets_filename) + self.assertFalse(os.path.isfile(widgets_path)) + css_path = os.path.join(home_dir, default_css_filename) + self.assertFalse(os.path.isfile(css_path)) + def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} user_dict["full_name"] = "Test User" @@ -581,23 +685,22 @@ def test_user_creation_creates_fs_entries(self): # TODO: add UUID to htaccess and enable next? # self.assertIn(req_pattern % TEST_USER_UUID, htaccess_contents) + enc_user_dn = TEST_USER_DN.encode('utf8') + enc_creator = 'CREATOR'.encode('utf8') welcome_path = os.path.join(home_dir, welcome_filename) self.assertTrue(os.path.isfile(welcome_path)) settings_path = os.path.join(settings_dir, settings_filename) self.assertTrue(os.path.isfile(settings_path)) pickled = self.assertPickledFile(settings_path) - self.assertIn(TEST_USER_DN.encode('utf8'), - pickled['CREATOR'.encode('utf8')]) + self.assertIn(enc_user_dn, pickled[enc_creator]) profile_path = os.path.join(settings_dir, profile_filename) self.assertTrue(os.path.isfile(profile_path)) pickled = self.assertPickledFile(profile_path) - self.assertIn(TEST_USER_DN.encode('utf8'), - pickled['CREATOR'.encode('utf8')]) + self.assertIn(enc_user_dn, pickled[enc_creator]) widgets_path = os.path.join(settings_dir, widgets_filename) self.assertTrue(os.path.isfile(widgets_path)) pickled = self.assertPickledFile(widgets_path) - self.assertIn(TEST_USER_DN.encode('utf8'), - pickled['CREATOR'.encode('utf8')]) + self.assertIn(enc_user_dn, pickled[enc_creator]) css_path = os.path.join(home_dir, default_css_filename) self.assertTrue(os.path.isfile(css_path)) with open(css_path) as test_fd: @@ -671,6 +774,111 @@ def test_user_creation_and_renew_records_a_user(self): except: self.assertFalse(True, "should not be reached") + def test_user_creation_and_delete_records_a_user(self): + user_dict = {} + user_dict["unique_id"] = TEST_USER_UUID + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + + try: + delete_user( + user_dict, + self.configuration, + keyword_auto, + force=True, + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_deletion_removes_fs_entries(self): + user_dict = {} + user_dict["unique_id"] = TEST_USER_UUID + user_dict["short_id"] = TEST_USER_SHORT_ID + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + try: + delete_user( + user_dict, + self.configuration, + keyword_auto, + force=True, + ) + except: + self.assertFalse(True, "should not be reached") + + home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) + self.assertFalse(os.path.isdir(home_dir)) + home_link = os.path.join(self.configuration.user_home, + TEST_USER_DIR) + self.assertFalse(os.path.islink(home_link)) + short_link = os.path.join(self.configuration.user_home, + TEST_USER_SHORT_ID) + self.assertFalse(os.path.islink(short_link)) + + settings_dir = os.path.join(self.configuration.user_settings, + TEST_USER_UUID) + self.assertFalse(os.path.isdir(settings_dir)) + settings_link = os.path.join(self.configuration.user_settings, + TEST_USER_DIR) + self.assertFalse(os.path.islink(settings_link)) + + ssh_dir = os.path.join(home_dir, ssh_conf_dir) + self.assertFalse(os.path.isdir(ssh_dir)) + davs_dir = os.path.join(home_dir, davs_conf_dir) + self.assertFalse(os.path.isdir(davs_dir)) + ftps_dir = os.path.join(home_dir, ftps_conf_dir) + self.assertFalse(os.path.isdir(ftps_dir)) + htaccess_path = os.path.join(home_dir, htaccess_filename) + self.assertFalse(os.path.isfile(htaccess_path)) + welcome_path = os.path.join(home_dir, welcome_filename) + self.assertFalse(os.path.isfile(welcome_path)) + settings_path = os.path.join(settings_dir, settings_filename) + self.assertFalse(os.path.isfile(settings_path)) + profile_path = os.path.join(settings_dir, profile_filename) + self.assertFalse(os.path.isfile(profile_path)) + widgets_path = os.path.join(settings_dir, widgets_filename) + self.assertFalse(os.path.isfile(widgets_path)) + css_path = os.path.join(home_dir, default_css_filename) + self.assertFalse(os.path.isfile(css_path)) + def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} user_dict["unique_id"] = TEST_USER_UUID From 3325a30f23bf2bf501f973b2247c33cc6b87c3c6 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 16:50:37 +0200 Subject: [PATCH 06/61] More thorough clean up verification. --- tests/test_mig_shared_useradm.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 668886da2..91fc1a701 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -438,40 +438,50 @@ def test_user_deletion_removes_fs_entries(self): except: self.assertFalse(True, "should not be reached") - home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) + home_dir = os.path.join(self.configuration.user_home, TEST_USER_DN) self.assertFalse(os.path.isdir(home_dir)) - home_link = os.path.join(self.configuration.user_home, - TEST_USER_DIR) - self.assertFalse(os.path.islink(home_link)) + self.assertFalse(os.path.exists(home_dir)) short_link = os.path.join(self.configuration.user_home, TEST_USER_SHORT_ID) self.assertFalse(os.path.islink(short_link)) + self.assertFalse(os.path.exists(short_link)) settings_dir = os.path.join(self.configuration.user_settings, TEST_USER_UUID) self.assertFalse(os.path.isdir(settings_dir)) + self.assertFalse(os.path.exists(settings_dir)) settings_link = os.path.join(self.configuration.user_settings, TEST_USER_DIR) self.assertFalse(os.path.islink(settings_link)) + self.assertFalse(os.path.exists(settings_link)) ssh_dir = os.path.join(home_dir, ssh_conf_dir) self.assertFalse(os.path.isdir(ssh_dir)) + self.assertFalse(os.path.exists(ssh_dir)) davs_dir = os.path.join(home_dir, davs_conf_dir) self.assertFalse(os.path.isdir(davs_dir)) + self.assertFalse(os.path.exists(davs_dir)) ftps_dir = os.path.join(home_dir, ftps_conf_dir) self.assertFalse(os.path.isdir(ftps_dir)) + self.assertFalse(os.path.exists(ftps_dir)) htaccess_path = os.path.join(home_dir, htaccess_filename) self.assertFalse(os.path.isfile(htaccess_path)) + self.assertFalse(os.path.exists(htaccess_path)) welcome_path = os.path.join(home_dir, welcome_filename) self.assertFalse(os.path.isfile(welcome_path)) + self.assertFalse(os.path.exists(welcome_path)) settings_path = os.path.join(settings_dir, settings_filename) self.assertFalse(os.path.isfile(settings_path)) + self.assertFalse(os.path.exists(settings_path)) profile_path = os.path.join(settings_dir, profile_filename) self.assertFalse(os.path.isfile(profile_path)) + self.assertFalse(os.path.exists(profile_path)) widgets_path = os.path.join(settings_dir, widgets_filename) self.assertFalse(os.path.isfile(widgets_path)) + self.assertFalse(os.path.exists(widgets_path)) css_path = os.path.join(home_dir, default_css_filename) self.assertFalse(os.path.isfile(css_path)) + self.assertFalse(os.path.exists(css_path)) def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} @@ -846,38 +856,52 @@ def test_user_deletion_removes_fs_entries(self): home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) self.assertFalse(os.path.isdir(home_dir)) + self.assertFalse(os.path.exists(home_dir)) home_link = os.path.join(self.configuration.user_home, TEST_USER_DIR) self.assertFalse(os.path.islink(home_link)) + self.assertFalse(os.path.exists(home_link)) short_link = os.path.join(self.configuration.user_home, TEST_USER_SHORT_ID) self.assertFalse(os.path.islink(short_link)) + self.assertFalse(os.path.exists(short_link)) settings_dir = os.path.join(self.configuration.user_settings, TEST_USER_UUID) self.assertFalse(os.path.isdir(settings_dir)) + self.assertFalse(os.path.exists(settings_dir)) settings_link = os.path.join(self.configuration.user_settings, TEST_USER_DIR) self.assertFalse(os.path.islink(settings_link)) + self.assertFalse(os.path.exists(settings_link)) ssh_dir = os.path.join(home_dir, ssh_conf_dir) self.assertFalse(os.path.isdir(ssh_dir)) + self.assertFalse(os.path.exists(ssh_dir)) davs_dir = os.path.join(home_dir, davs_conf_dir) self.assertFalse(os.path.isdir(davs_dir)) + self.assertFalse(os.path.exists(davs_dir)) ftps_dir = os.path.join(home_dir, ftps_conf_dir) self.assertFalse(os.path.isdir(ftps_dir)) + self.assertFalse(os.path.exists(ftps_dir)) htaccess_path = os.path.join(home_dir, htaccess_filename) self.assertFalse(os.path.isfile(htaccess_path)) + self.assertFalse(os.path.exists(htaccess_path)) welcome_path = os.path.join(home_dir, welcome_filename) self.assertFalse(os.path.isfile(welcome_path)) + self.assertFalse(os.path.exists(welcome_path)) settings_path = os.path.join(settings_dir, settings_filename) self.assertFalse(os.path.isfile(settings_path)) + self.assertFalse(os.path.exists(settings_path)) profile_path = os.path.join(settings_dir, profile_filename) self.assertFalse(os.path.isfile(profile_path)) + self.assertFalse(os.path.exists(profile_path)) widgets_path = os.path.join(settings_dir, widgets_filename) self.assertFalse(os.path.isfile(widgets_path)) + self.assertFalse(os.path.exists(widgets_path)) css_path = os.path.join(home_dir, default_css_filename) self.assertFalse(os.path.isfile(css_path)) + self.assertFalse(os.path.exists(css_path)) def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} From 742cc2504f4f0066f54e5fc09c0de34a6db2cbb3 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 28 May 2026 17:00:17 +0200 Subject: [PATCH 07/61] Split tests into more logical parts with delete_user tested separately. --- tests/test_mig_shared_useradm.py | 496 ++++++++++++++++++++++--------- 1 file changed, 359 insertions(+), 137 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 91fc1a701..3bce0e27d 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -369,120 +369,6 @@ def test_user_creation_and_renew_records_a_user(self): except Exception: self.assertFalse(True, "should not be reached") - def test_user_creation_and_delete_records_a_user(self): - user_dict = {} - user_dict["full_name"] = "Test User" - user_dict["organization"] = "Test Org" - user_dict["state"] = "NA" - user_dict["country"] = "DK" - user_dict["email"] = "test@example.com" - user_dict["comment"] = "This is the create comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH - - try: - create_user( - user_dict, - self.configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - except: - self.assertFalse(True, "should not be reached") - - try: - delete_user( - user_dict, - self.configuration, - keyword_auto, - force=True, - ) - except: - self.assertFalse(True, "should not be reached") - - def test_user_deletion_removes_fs_entries(self): - user_dict = {} - user_dict["unique_id"] = TEST_USER_UUID - user_dict["short_id"] = TEST_USER_SHORT_ID - user_dict["full_name"] = "Test User" - user_dict["organization"] = "Test Org" - user_dict["state"] = "NA" - user_dict["country"] = "DK" - user_dict["email"] = "test@example.com" - user_dict["comment"] = "This is the create comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH - - try: - create_user( - user_dict, - self.configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - except: - self.assertFalse(True, "should not be reached") - try: - delete_user( - user_dict, - self.configuration, - keyword_auto, - force=True, - ) - except: - self.assertFalse(True, "should not be reached") - - home_dir = os.path.join(self.configuration.user_home, TEST_USER_DN) - self.assertFalse(os.path.isdir(home_dir)) - self.assertFalse(os.path.exists(home_dir)) - short_link = os.path.join(self.configuration.user_home, - TEST_USER_SHORT_ID) - self.assertFalse(os.path.islink(short_link)) - self.assertFalse(os.path.exists(short_link)) - - settings_dir = os.path.join(self.configuration.user_settings, - TEST_USER_UUID) - self.assertFalse(os.path.isdir(settings_dir)) - self.assertFalse(os.path.exists(settings_dir)) - settings_link = os.path.join(self.configuration.user_settings, - TEST_USER_DIR) - self.assertFalse(os.path.islink(settings_link)) - self.assertFalse(os.path.exists(settings_link)) - - ssh_dir = os.path.join(home_dir, ssh_conf_dir) - self.assertFalse(os.path.isdir(ssh_dir)) - self.assertFalse(os.path.exists(ssh_dir)) - davs_dir = os.path.join(home_dir, davs_conf_dir) - self.assertFalse(os.path.isdir(davs_dir)) - self.assertFalse(os.path.exists(davs_dir)) - ftps_dir = os.path.join(home_dir, ftps_conf_dir) - self.assertFalse(os.path.isdir(ftps_dir)) - self.assertFalse(os.path.exists(ftps_dir)) - htaccess_path = os.path.join(home_dir, htaccess_filename) - self.assertFalse(os.path.isfile(htaccess_path)) - self.assertFalse(os.path.exists(htaccess_path)) - welcome_path = os.path.join(home_dir, welcome_filename) - self.assertFalse(os.path.isfile(welcome_path)) - self.assertFalse(os.path.exists(welcome_path)) - settings_path = os.path.join(settings_dir, settings_filename) - self.assertFalse(os.path.isfile(settings_path)) - self.assertFalse(os.path.exists(settings_path)) - profile_path = os.path.join(settings_dir, profile_filename) - self.assertFalse(os.path.isfile(profile_path)) - self.assertFalse(os.path.exists(profile_path)) - widgets_path = os.path.join(settings_dir, widgets_filename) - self.assertFalse(os.path.isfile(widgets_path)) - self.assertFalse(os.path.exists(widgets_path)) - css_path = os.path.join(home_dir, default_css_filename) - self.assertFalse(os.path.isfile(css_path)) - self.assertFalse(os.path.exists(css_path)) - def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} user_dict["full_name"] = "Test User" @@ -784,7 +670,7 @@ def test_user_creation_and_renew_records_a_user(self): except: self.assertFalse(True, "should not be reached") - def test_user_creation_and_delete_records_a_user(self): + def test_user_creation_fails_in_renew_when_locked(self): user_dict = {} user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" @@ -797,6 +683,260 @@ def test_user_creation_and_delete_records_a_user(self): user_dict["organizational_unit"] = "" user_dict["password"] = "" user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + # explicitly setting set a DN suffixed user DN to force GDP + user_dict["distinguished_name"] = self.TEST_USER_DN_GDP + user_dict["status"] = "locked" + + try: + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_creation_with_id_collission_fails(self): + user_dict = {} + user_dict["unique_id"] = TEST_USER_UUID + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "user@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["password"] = "password" + user_dict["distinguished_name"] = TEST_USER_DN + + try: + create_user( + user_dict, self.configuration, keyword_auto, default_renew=True + ) + except: + self.assertFalse(True, "should not be reached") + + # NOTE: reset distinguished_name and introduce an ID conflict to test + del user_dict["distinguished_name"] + user_dict["organization"] = "Another Org" + with self.assertRaises(Exception): + create_user( + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + + +class MigSharedUseradm__assure_current_htaccess(MigTestCase): + """Coverage of useradm behaviours around htaccess.""" + + def before_each(self): + """The create_user call requires quite a few helper dirs""" + os.makedirs(os.path.join(DUMMY_HOME_PATH, DUMMY_USER)) + os.makedirs(os.path.join(DUMMY_SETTINGS_PATH, DUMMY_USER)) + os.makedirs(os.path.join(DUMMY_MRSL_FILES_PATH, DUMMY_USER)) + os.makedirs(os.path.join(DUMMY_RESOURCE_PENDING_PATH, DUMMY_USER)) + os.makedirs(os.path.join(DUMMY_CACHE_PATH, DUMMY_USER)) + cleanpath(DUMMY_HOME_PATH, self) + cleanpath(DUMMY_SETTINGS_PATH, self) + cleanpath(DUMMY_MRSL_FILES_PATH, self) + cleanpath(DUMMY_RESOURCE_PENDING_PATH, self) + cleanpath(DUMMY_CACHE_PATH, self) + + def assertHtaccessRequireUserClause(self, generated, expected): + """Makes sure generated htaccess file contains the expected string""" + if isinstance(generated, basestring): + with io.open(generated) as htaccess_file: + generated = htaccess_file.read() + + generated_lines = generated.split("\n") + if not expected in generated_lines: + raise AssertionError("no such require user line: %s" % expected) + + def test_skips_accounts_without_short_id(self): + user_dict = {} + user_dict.update(DUMMY_USER_DICT) + del user_dict["short_id"] + assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, + False) + + try: + path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) + # File should not exist here at all + self.assertNotEqual(path_kind, "file") + except OSError as ignore_oserr: + pass + + def test_creates_missing_htaccess_file(self): + user_dict = {} + user_dict.update(DUMMY_USER_DICT) + assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, + False) + + path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) + # File should exist here and be valid + self.assertEqual(path_kind, "file") + path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_BACKUP_PATH) + # Backup file should exist here and be empty + self.assertEqual(path_kind, "file") + + self.assertHtaccessRequireUserClause( + DUMMY_HTACCESS_PATH, DUMMY_REQUIRE_USER + ) + + def test_repairs_existing_stale_htaccess_file(self): + user_dict = {} + user_dict.update(DUMMY_USER_DICT) + # Fake stale user ID directly through DN + user_dict["distinguished_name"] = DUMMY_STALE_USER + assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, + False) + + # Verify stale + self.assertHtaccessRequireUserClause( + DUMMY_HTACCESS_PATH, DUMMY_REQUIRE_STALE_USER + ) + + # Reset stale user ID and retry + user_dict = {} + user_dict.update(DUMMY_USER_DICT) + assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, + False) + + path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) + # File should exist here and be valid + self.assertEqual(path_kind, "file") + path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_BACKUP_PATH) + # Backup file should exist here and be empty + self.assertEqual(path_kind, "file") + + self.assertHtaccessRequireUserClause( + DUMMY_HTACCESS_PATH, DUMMY_REQUIRE_USER + ) + + +class TestMigSharedUseradm__user_account_notify(MigTestCase, UserAssertMixin): + """Coverage of useradm user_account_notify function.""" + + expected_expire = -1 + + def _provide_configuration(self): + """Return configuration to use""" + return "testconfig" + + def before_each(self): + """Create test environment for useradm tests""" + configuration = self.configuration + + _ensure_dirs_needed_for_userdb(self.configuration) + + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + self._provision_test_user(self, TEST_USER_DN) + adjusted_datetime = datetime.date.today() + datetime.timedelta(days=5) + self.expected_expire = int(time.mktime(adjusted_datetime.timetuple())) + + def test_default_address_and_expire(self): + """Test addresses and expire for test account""" + (_, username, full_name, expire, addresses, errors) = \ + user_account_notify(TEST_USER_DN, {'email': ['AUTO']}, + self.configuration, + self.expected_user_db_file, False, + False) + self.assertEqual(addresses, {'email': [TEST_USER_EMAIL]}) + self.assertEqual(expire, self.expected_expire) + self.assertEqual(errors, []) + + def test_extra_address_and_expire(self): + """Test addresses and expire for test with extra account""" + (_, username, full_name, expire, addresses, errors) = \ + user_account_notify(TEST_USER_DN, {'email': + ['AUTO', OTHER_USER_EMAIL]}, + self.configuration, + self.expected_user_db_file, False, + False) + self.assertEqual(addresses, {'email': + [TEST_USER_EMAIL, OTHER_USER_EMAIL]}) + self.assertEqual(expire, self.expected_expire) + self.assertEqual(errors, []) + + def test_missing_user_fails(self): + """Test failure for missing user account""" + (_, username, full_name, expire, addresses, errors) = \ + user_account_notify(OTHER_USER_DN, {'email': ['AUTO']}, + self.configuration, + self.expected_user_db_file, False, + False) + self.assertEqual(addresses, {'email': []}) + self.assertEqual(expire, None) + self.assertTrue(errors and 'No such user' in errors[0]) + + def test_missing_user_db_bails_out(self): + """Test failure for missing user db""" + with self.assertLogs(level='ERROR') as log_capture: + (_, username, full_name, expire, addresses, errors) = \ + user_account_notify(OTHER_USER_DN, {'email': ['AUTO']}, + self.configuration, NO_SUCH_USER_DB, + False, False) + self.assertEqual(addresses, []) + self.assertEqual(expire, None) + self.assertTrue(errors and 'Failed to load user DB' in errors[0]) + self.assertTrue(any('Failed to load user DB' in msg for msg in + log_capture.output)) + try: + os.remove("%s.lock" % NO_SUCH_USER_DB) + except Exception: + pass + + +# TODO: consider merging the tests for UUID and X509 format +class TestMigSharedUseradm__delete_user( + MigTestCase, FixtureAssertMixin, PickleAssertMixin +): + """Coverage of useradm delete_user function.""" + + TEST_USER_DN_GDP = "%s/GDP" % (TEST_USER_DN,) + TEST_USER_PASSWORD_HASH = ( + "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM2134uix" + ) + + def before_each(self): + configuration = self.configuration + configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT + + _ensure_dirs_needed_for_userdb(self.configuration) + + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def _provide_configuration(self): + return "testconfig" + + def test_user_delete_completes(self): + user_dict = {} + user_dict["full_name"] = "Test User" + user_dict["organization"] = "Test Org" + user_dict["state"] = "NA" + user_dict["country"] = "DK" + user_dict["email"] = "test@example.com" + user_dict["comment"] = "This is the create comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH try: create_user( @@ -854,13 +994,9 @@ def test_user_deletion_removes_fs_entries(self): except: self.assertFalse(True, "should not be reached") - home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) + home_dir = os.path.join(self.configuration.user_home, TEST_USER_DN) self.assertFalse(os.path.isdir(home_dir)) self.assertFalse(os.path.exists(home_dir)) - home_link = os.path.join(self.configuration.user_home, - TEST_USER_DIR) - self.assertFalse(os.path.islink(home_link)) - self.assertFalse(os.path.exists(home_link)) short_link = os.path.join(self.configuration.user_home, TEST_USER_SHORT_ID) self.assertFalse(os.path.islink(short_link)) @@ -903,7 +1039,33 @@ def test_user_deletion_removes_fs_entries(self): self.assertFalse(os.path.isfile(css_path)) self.assertFalse(os.path.exists(css_path)) - def test_user_creation_fails_in_renew_when_locked(self): + +class TestMigSharedUseradm__delete_user_uuid_user_id( + MigTestCase, FixtureAssertMixin, PickleAssertMixin +): + """Coverage of useradm delete_user function with UUID format.""" + + TEST_USER_DN_GDP = "%s/GDP" % (TEST_USER_DN,) + TEST_USER_PASSWORD_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM2134uix" + + def before_each(self): + configuration = self.configuration + configuration.site_user_id_format = UUID_USER_ID_FORMAT + + _ensure_dirs_needed_for_userdb(self.configuration) + + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def _provide_configuration(self): + return "testconfig" + + def test_user_delete_completes(self): user_dict = {} user_dict["unique_id"] = TEST_USER_UUID user_dict["full_name"] = "Test User" @@ -916,9 +1078,6 @@ def test_user_creation_fails_in_renew_when_locked(self): user_dict["organizational_unit"] = "" user_dict["password"] = "" user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH - # explicitly setting set a DN suffixed user DN to force GDP - user_dict["distinguished_name"] = self.TEST_USER_DN_GDP - user_dict["status"] = "locked" try: create_user( @@ -931,36 +1090,99 @@ def test_user_creation_fails_in_renew_when_locked(self): except: self.assertFalse(True, "should not be reached") - def test_user_creation_with_id_collission_fails(self): + try: + delete_user( + user_dict, + self.configuration, + keyword_auto, + force=True, + ) + except: + self.assertFalse(True, "should not be reached") + + def test_user_deletion_removes_fs_entries(self): user_dict = {} user_dict["unique_id"] = TEST_USER_UUID + user_dict["short_id"] = TEST_USER_SHORT_ID user_dict["full_name"] = "Test User" user_dict["organization"] = "Test Org" user_dict["state"] = "NA" user_dict["country"] = "DK" - user_dict["email"] = "user@example.com" + user_dict["email"] = "test@example.com" user_dict["comment"] = "This is the create comment" - user_dict["password"] = "password" - user_dict["distinguished_name"] = TEST_USER_DN + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH try: create_user( - user_dict, self.configuration, keyword_auto, default_renew=True + user_dict, + self.configuration, + keyword_auto, + default_renew=True, + ask_renew=False, ) except: self.assertFalse(True, "should not be reached") - - # NOTE: reset distinguished_name and introduce an ID conflict to test - del user_dict["distinguished_name"] - user_dict["organization"] = "Another Org" - with self.assertRaises(Exception): - create_user( + try: + delete_user( user_dict, self.configuration, keyword_auto, - default_renew=True, - ask_renew=False, + force=True, ) + except: + self.assertFalse(True, "should not be reached") + + home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) + self.assertFalse(os.path.isdir(home_dir)) + self.assertFalse(os.path.exists(home_dir)) + home_link = os.path.join(self.configuration.user_home, + TEST_USER_DIR) + self.assertFalse(os.path.islink(home_link)) + self.assertFalse(os.path.exists(home_link)) + short_link = os.path.join(self.configuration.user_home, + TEST_USER_SHORT_ID) + self.assertFalse(os.path.islink(short_link)) + self.assertFalse(os.path.exists(short_link)) + + settings_dir = os.path.join(self.configuration.user_settings, + TEST_USER_UUID) + self.assertFalse(os.path.isdir(settings_dir)) + self.assertFalse(os.path.exists(settings_dir)) + settings_link = os.path.join(self.configuration.user_settings, + TEST_USER_DIR) + self.assertFalse(os.path.islink(settings_link)) + self.assertFalse(os.path.exists(settings_link)) + + ssh_dir = os.path.join(home_dir, ssh_conf_dir) + self.assertFalse(os.path.isdir(ssh_dir)) + self.assertFalse(os.path.exists(ssh_dir)) + davs_dir = os.path.join(home_dir, davs_conf_dir) + self.assertFalse(os.path.isdir(davs_dir)) + self.assertFalse(os.path.exists(davs_dir)) + ftps_dir = os.path.join(home_dir, ftps_conf_dir) + self.assertFalse(os.path.isdir(ftps_dir)) + self.assertFalse(os.path.exists(ftps_dir)) + htaccess_path = os.path.join(home_dir, htaccess_filename) + self.assertFalse(os.path.isfile(htaccess_path)) + self.assertFalse(os.path.exists(htaccess_path)) + welcome_path = os.path.join(home_dir, welcome_filename) + self.assertFalse(os.path.isfile(welcome_path)) + self.assertFalse(os.path.exists(welcome_path)) + settings_path = os.path.join(settings_dir, settings_filename) + self.assertFalse(os.path.isfile(settings_path)) + self.assertFalse(os.path.exists(settings_path)) + profile_path = os.path.join(settings_dir, profile_filename) + self.assertFalse(os.path.isfile(profile_path)) + self.assertFalse(os.path.exists(profile_path)) + widgets_path = os.path.join(settings_dir, widgets_filename) + self.assertFalse(os.path.isfile(widgets_path)) + self.assertFalse(os.path.exists(widgets_path)) + css_path = os.path.join(home_dir, default_css_filename) + self.assertFalse(os.path.isfile(css_path)) + self.assertFalse(os.path.exists(css_path)) class MigSharedUseradm__assure_current_htaccess(MigTestCase): From 4bb7f6db62e9ee6ec22e285f1f1d1626ef325e75 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 29 May 2026 23:14:27 +0200 Subject: [PATCH 08/61] Rework delete_user unit tests to rely on _provision_test_user for simplicity. Add unit tests for edit_user both in the default and UUID format setup to cover more of the ground needed for supporting the latter. --- tests/test_mig_shared_useradm.py | 875 ++++++++++++++++++------------- 1 file changed, 507 insertions(+), 368 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 3bce0e27d..08ed830c9 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -138,6 +138,34 @@ ) +def _provision_uuid_test_user(configuration, client_id, client_overrides=None): + """Helper to provision test users when UUID format is used""" + # TODO: merge something like this version into standard _provision_test_user? + # IMPORTANT: we need to use explicit create_user here for UUID format! + user_dict = distinguished_name_to_user(client_id) + # NOTE: generate unique and short id based on id to avoid test collisions + user_dict["unique_id"] = binascii.hexlify( + client_id.encode('utf8')).decode('ascii') + user_dict["short_id"] = binascii.hexlify( + user_dict["email"].encode('utf8')).decode('ascii') + user_dict["comment"] = "This is the user account comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = "" + if client_overrides is not None: + user_dict.update(client_overrides) + + create_user( + user_dict, + configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + return user_dict + + class TestMigSharedUseradm__create_user( MigTestCase, FixtureAssertMixin, PickleAssertMixin ): @@ -926,72 +954,29 @@ def _provide_configuration(self): return "testconfig" def test_user_delete_completes(self): - user_dict = {} - user_dict["full_name"] = "Test User" - user_dict["organization"] = "Test Org" - user_dict["state"] = "NA" - user_dict["country"] = "DK" - user_dict["email"] = "test@example.com" - user_dict["comment"] = "This is the create comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH - - try: - create_user( - user_dict, - self.configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - except: - self.assertFalse(True, "should not be reached") + self._provision_test_user(self, TEST_USER_DN) try: delete_user( - user_dict, + distinguished_name_to_user(TEST_USER_DN), self.configuration, keyword_auto, force=True, ) - except: + except Exception: self.assertFalse(True, "should not be reached") def test_user_deletion_removes_fs_entries(self): - user_dict = {} - user_dict["unique_id"] = TEST_USER_UUID - user_dict["short_id"] = TEST_USER_SHORT_ID - user_dict["full_name"] = "Test User" - user_dict["organization"] = "Test Org" - user_dict["state"] = "NA" - user_dict["country"] = "DK" - user_dict["email"] = "test@example.com" - user_dict["comment"] = "This is the create comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + self._provision_test_user(self, TEST_USER_DN) - try: - create_user( - user_dict, - self.configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - except: - self.assertFalse(True, "should not be reached") try: delete_user( - user_dict, + distinguished_name_to_user(TEST_USER_DN), self.configuration, keyword_auto, force=True, ) - except: + except Exception: self.assertFalse(True, "should not be reached") home_dir = os.path.join(self.configuration.user_home, TEST_USER_DN) @@ -1066,73 +1051,29 @@ def _provide_configuration(self): return "testconfig" def test_user_delete_completes(self): - user_dict = {} - user_dict["unique_id"] = TEST_USER_UUID - user_dict["full_name"] = "Test User" - user_dict["organization"] = "Test Org" - user_dict["state"] = "NA" - user_dict["country"] = "DK" - user_dict["email"] = "test@example.com" - user_dict["comment"] = "This is the create comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH - - try: - create_user( - user_dict, - self.configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - except: - self.assertFalse(True, "should not be reached") + _provision_uuid_test_user(self.configuration, TEST_USER_DN) try: delete_user( - user_dict, + distinguished_name_to_user(TEST_USER_DN), self.configuration, keyword_auto, force=True, ) - except: + except Exception: self.assertFalse(True, "should not be reached") def test_user_deletion_removes_fs_entries(self): - user_dict = {} - user_dict["unique_id"] = TEST_USER_UUID - user_dict["short_id"] = TEST_USER_SHORT_ID - user_dict["full_name"] = "Test User" - user_dict["organization"] = "Test Org" - user_dict["state"] = "NA" - user_dict["country"] = "DK" - user_dict["email"] = "test@example.com" - user_dict["comment"] = "This is the create comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH + _provision_uuid_test_user(self.configuration, TEST_USER_DN) - try: - create_user( - user_dict, - self.configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - except: - self.assertFalse(True, "should not be reached") try: delete_user( - user_dict, + distinguished_name_to_user(TEST_USER_DN), self.configuration, keyword_auto, force=True, ) - except: + except Exception: self.assertFalse(True, "should not be reached") home_dir = os.path.join(self.configuration.user_home, TEST_USER_UUID) @@ -1185,106 +1126,18 @@ def test_user_deletion_removes_fs_entries(self): self.assertFalse(os.path.exists(css_path)) -class MigSharedUseradm__assure_current_htaccess(MigTestCase): - """Coverage of useradm behaviours around htaccess.""" - - def before_each(self): - """The create_user call requires quite a few helper dirs""" - os.makedirs(os.path.join(DUMMY_HOME_PATH, DUMMY_USER)) - os.makedirs(os.path.join(DUMMY_SETTINGS_PATH, DUMMY_USER)) - os.makedirs(os.path.join(DUMMY_MRSL_FILES_PATH, DUMMY_USER)) - os.makedirs(os.path.join(DUMMY_RESOURCE_PENDING_PATH, DUMMY_USER)) - os.makedirs(os.path.join(DUMMY_CACHE_PATH, DUMMY_USER)) - cleanpath(DUMMY_HOME_PATH, self) - cleanpath(DUMMY_SETTINGS_PATH, self) - cleanpath(DUMMY_MRSL_FILES_PATH, self) - cleanpath(DUMMY_RESOURCE_PENDING_PATH, self) - cleanpath(DUMMY_CACHE_PATH, self) - - def assertHtaccessRequireUserClause(self, generated, expected): - """Makes sure generated htaccess file contains the expected string""" - if isinstance(generated, basestring): - with io.open(generated) as htaccess_file: - generated = htaccess_file.read() - - generated_lines = generated.split("\n") - if expected not in generated_lines: - raise AssertionError("no such require user line: %s" % expected) - - def test_skips_accounts_without_short_id(self): - user_dict = {} - user_dict.update(DUMMY_USER_DICT) - del user_dict["short_id"] - assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, - False) - - try: - path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) - # File should not exist here at all - self.assertNotEqual(path_kind, "file") - except OSError: - pass - - def test_creates_missing_htaccess_file(self): - user_dict = {} - user_dict.update(DUMMY_USER_DICT) - assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, - False) - - path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) - # File should exist here and be valid - self.assertEqual(path_kind, "file") - path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_BACKUP_PATH) - # Backup file should exist here and be empty - self.assertEqual(path_kind, "file") - - self.assertHtaccessRequireUserClause( - DUMMY_HTACCESS_PATH, DUMMY_REQUIRE_USER - ) - - def test_repairs_existing_stale_htaccess_file(self): - user_dict = {} - user_dict.update(DUMMY_USER_DICT) - # Fake stale user ID directly through DN - user_dict["distinguished_name"] = DUMMY_STALE_USER - assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, - False) - - # Verify stale - self.assertHtaccessRequireUserClause( - DUMMY_HTACCESS_PATH, DUMMY_REQUIRE_STALE_USER - ) - - # Reset stale user ID and retry - user_dict = {} - user_dict.update(DUMMY_USER_DICT) - assure_current_htaccess(DUMMY_CONF, DUMMY_USER, user_dict, False, - False) - - path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) - # File should exist here and be valid - self.assertEqual(path_kind, "file") - path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_BACKUP_PATH) - # Backup file should exist here and be empty - self.assertEqual(path_kind, "file") - - self.assertHtaccessRequireUserClause( - DUMMY_HTACCESS_PATH, DUMMY_REQUIRE_USER - ) - - -class TestMigSharedUseradm__user_account_notify(MigTestCase, UserAssertMixin): - """Coverage of useradm user_account_notify function.""" - - expected_expire = -1 +class TestMigSharedUseradm__edit_user( + MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin +): + """Coverage of useradm edit_user function.""" - def _provide_configuration(self): - """Return configuration to use""" - return "testconfig" + TEST_USER_PASSWORD_HASH = ( + "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM2134uix" + ) def before_each(self): - """Create test environment for useradm tests""" configuration = self.configuration + configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT _ensure_dirs_needed_for_userdb(self.configuration) @@ -1295,162 +1148,232 @@ def before_each(self): self.expected_user_db_home, "MiG-users.db" ) ensure_dirs_exist(self.configuration.mig_system_files) - self._provision_test_user(self, TEST_USER_DN) - adjusted_datetime = datetime.date.today() + datetime.timedelta(days=5) - self.expected_expire = int(time.mktime(adjusted_datetime.timetuple())) + ensure_dirs_exist(self.configuration.resource_home) - def test_default_address_and_expire(self): - """Test addresses and expire for test account""" - (_, username, full_name, expire, addresses, errors) = \ - user_account_notify(TEST_USER_DN, {'email': ['AUTO']}, - self.configuration, - self.expected_user_db_file, False, - False) - self.assertEqual(addresses, {'email': [TEST_USER_EMAIL]}) - self.assertEqual(expire, self.expected_expire) - self.assertEqual(errors, []) + def _provide_configuration(self): + return "testconfig" - def test_extra_address_and_expire(self): - """Test addresses and expire for test with extra account""" - (_, username, full_name, expire, addresses, errors) = \ - user_account_notify(TEST_USER_DN, {'email': - ['AUTO', OTHER_USER_EMAIL]}, - self.configuration, - self.expected_user_db_file, False, - False) - self.assertEqual(addresses, {'email': - [TEST_USER_EMAIL, OTHER_USER_EMAIL]}) - self.assertEqual(expire, self.expected_expire) - self.assertEqual(errors, []) + def _flush_test_user(self, client_id): + """Helper to force clean up after provisioned test users""" + try: + delete_user( + {'distinguished_name': client_id}, + self.configuration, + keyword_auto, + force=True, + ) + except Exception: + pass - def test_missing_user_fails(self): - """Test failure for missing user account""" - (_, username, full_name, expire, addresses, errors) = \ - user_account_notify(OTHER_USER_DN, {'email': ['AUTO']}, - self.configuration, - self.expected_user_db_file, False, - False) - self.assertEqual(addresses, {'email': []}) - self.assertEqual(expire, None) - self.assertTrue(errors and 'No such user' in errors[0]) + def test_edit_user_email(self): + """Test basic user email editing""" + self._provision_test_user(self, TEST_USER_DN) + # Extract original for later verification + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + user_dict = pickled[TEST_USER_DN] - def test_missing_user_db_bails_out(self): - """Test failure for missing user db""" - with self.assertLogs(level='ERROR') as log_capture: - (_, username, full_name, expire, addresses, errors) = \ - user_account_notify(OTHER_USER_DN, {'email': ['AUTO']}, - self.configuration, NO_SUCH_USER_DB, - False, False) - self.assertEqual(addresses, []) - self.assertEqual(expire, None) - self.assertTrue(errors and 'Failed to load user DB' in errors[0]) - self.assertTrue(any('Failed to load user DB' in msg for msg in - log_capture.output)) - try: - os.remove("%s.lock" % NO_SUCH_USER_DB) - except Exception: - pass + # Edit user attribute and verify changed in DB + changes = { + "email": OTHER_USER_EMAIL, + } + removes = [] + edit_user( + TEST_USER_DN, changes, removes, self.configuration, keyword_auto + ) + edited_dn = TEST_USER_DN.replace(TEST_USER_EMAIL, OTHER_USER_EMAIL) + # Verify changes + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + self.assertIn(edited_dn, pickled) + edited_user = pickled[edited_dn] + for field in user_dict: + if field == "email": + self.assertEqual(edited_user[field], OTHER_USER_EMAIL) + elif field == "distinguished_name": + self.assertEqual(edited_user[field], edited_dn) + else: + self.assertEqual(edited_user[field], user_dict[field]) + self._flush_test_user(edited_user['distinguished_name']) -class TestMigSharedUseradm__delete_user( - MigTestCase, FixtureAssertMixin, PickleAssertMixin -): - """Coverage of useradm delete_user function.""" + def test_edit_user_remove_attributes(self): + """Test removing user attributes""" + self._provision_test_user(self, TEST_USER_DN) + # Extract original for later verification + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + user_dict = pickled[TEST_USER_DN] - TEST_USER_DN_GDP = "%s/GDP" % (TEST_USER_DN,) - TEST_USER_PASSWORD_HASH = ( - "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM2134uix" - ) + changes = {} + removes = ["comment"] + edit_user( + TEST_USER_DN, changes, removes, self.configuration, keyword_auto, + meta_only=True + ) - def before_each(self): - configuration = self.configuration - configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + edited_user = pickled[TEST_USER_DN] + for field in user_dict: + if field == "comment": + self.assertNotIn(field, edited_user) + else: + self.assertEqual(edited_user[field], user_dict[field]) - _ensure_dirs_needed_for_userdb(self.configuration) + self._flush_test_user(TEST_USER_DN) - self.expected_user_db_home = os.path.normpath( - configuration.user_db_home + def test_edit_user_meta_only(self): + """Test metadata-only update (no FS changes)""" + self._provision_test_user(self, TEST_USER_DN) + # Extract original for later verification + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" + user_dict = pickled[TEST_USER_DN] + + changes = {"comment": "meta comment"} + removes = [] + edit_user( + TEST_USER_DN, changes, removes, self.configuration, keyword_auto, + meta_only=True ) - ensure_dirs_exist(self.configuration.mig_system_files) - def _provide_configuration(self): - return "testconfig" + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + edited_user = pickled[TEST_USER_DN] + for field in user_dict: + if field == "comment": + self.assertEqual(pickled[TEST_USER_DN][field], "meta comment") + else: + self.assertEqual(edited_user[field], user_dict[field]) - def test_user_delete_completes(self): - self._provision_test_user(self, TEST_USER_DN) + self._flush_test_user(TEST_USER_DN) + @unittest.skip("make backend function less noisy (traceback) and enable?") + def test_edit_user_nonexistent(self): + """Test editing nonexistent user""" + changes = {"email": "dummy@example.com"} + removes = [] + with self.assertRaises(Exception): + edit_user( + NO_SUCH_USER_DN, changes, removes, self.configuration, + keyword_auto + ) + + @unittest.skip("make backend function less noisy (traceback) and enable?") + def test_edit_user_nonexistent_force(self): + """Test editing nonexistent user with force=True""" + changes = {"email": "dummy@example.com"} + removes = [] + # NOTE: forced edit logs errors in update_account_X functions + self.logger.forgive_errors() + with self.assertRaises(Exception): + edit_user( + NO_SUCH_USER_DN, changes, removes, self.configuration, + keyword_auto, force=True + ) + + @unittest.skip("make backend function less noisy (traceback) and enable?") + def test_edit_user_dn_collision_fails(self): + """Test that editing a user to a DN that already exists fails""" + # NOTE: we can't use _provision_test_users here as it lacks passwords + test_user_dict = distinguished_name_to_user(TEST_USER_DN) + test_user_dict["comment"] = "This is the create comment" + test_user_dict["password"] = "" + test_user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH try: - delete_user( - distinguished_name_to_user(TEST_USER_DN), - self.configuration, - keyword_auto, - force=True, + create_user( + test_user_dict, self.configuration, keyword_auto, default_renew=True ) except Exception: self.assertFalse(True, "should not be reached") - - def test_user_deletion_removes_fs_entries(self): - self._provision_test_user(self, TEST_USER_DN) - + other_user_dict = distinguished_name_to_user(OTHER_USER_DN) + other_user_dict["comment"] = "This is the create comment" + other_user_dict["password"] = "" + other_user_dict["password_hash"] = DUMMY_PASSWORD_HASH try: - delete_user( - distinguished_name_to_user(TEST_USER_DN), - self.configuration, - keyword_auto, - force=True, + create_user( + other_user_dict, self.configuration, keyword_auto, default_renew=True ) except Exception: self.assertFalse(True, "should not be reached") - home_dir = os.path.join(self.configuration.user_home, TEST_USER_DIR) - self.assertFalse(os.path.isdir(home_dir)) - self.assertFalse(os.path.exists(home_dir)) - short_link = os.path.join(self.configuration.user_home, - TEST_USER_SHORT_ID) - self.assertFalse(os.path.islink(short_link)) - self.assertFalse(os.path.exists(short_link)) + changes = distinguished_name_to_user(OTHER_USER_DN) + removes = [] + with self.assertRaises(Exception): + edit_user( + TEST_USER_DN, changes, removes, self.configuration, + keyword_auto + ) - settings_dir = os.path.join(self.configuration.user_settings, - TEST_USER_DIR) - self.assertFalse(os.path.isdir(settings_dir)) - self.assertFalse(os.path.exists(settings_dir)) + self._flush_test_user(TEST_USER_DN) + self._flush_test_user(OTHER_USER_DN) - ssh_dir = os.path.join(home_dir, ssh_conf_dir) - self.assertFalse(os.path.isdir(ssh_dir)) - self.assertFalse(os.path.exists(ssh_dir)) - davs_dir = os.path.join(home_dir, davs_conf_dir) - self.assertFalse(os.path.isdir(davs_dir)) - self.assertFalse(os.path.exists(davs_dir)) - ftps_dir = os.path.join(home_dir, ftps_conf_dir) - self.assertFalse(os.path.isdir(ftps_dir)) - self.assertFalse(os.path.exists(ftps_dir)) - htaccess_path = os.path.join(home_dir, htaccess_filename) - self.assertFalse(os.path.isfile(htaccess_path)) - self.assertFalse(os.path.exists(htaccess_path)) - welcome_path = os.path.join(home_dir, welcome_filename) - self.assertFalse(os.path.isfile(welcome_path)) - self.assertFalse(os.path.exists(welcome_path)) - settings_path = os.path.join(settings_dir, settings_filename) - self.assertFalse(os.path.isfile(settings_path)) - self.assertFalse(os.path.exists(settings_path)) - profile_path = os.path.join(settings_dir, profile_filename) - self.assertFalse(os.path.isfile(profile_path)) - self.assertFalse(os.path.exists(profile_path)) - widgets_path = os.path.join(settings_dir, widgets_filename) - self.assertFalse(os.path.isfile(widgets_path)) - self.assertFalse(os.path.exists(widgets_path)) - css_path = os.path.join(home_dir, default_css_filename) - self.assertFalse(os.path.isfile(css_path)) - self.assertFalse(os.path.exists(css_path)) + def test_edit_user_renames_user(self): + """Test editing fields that change the distinguished name""" + self._provision_test_user(self, TEST_USER_DN) + # Extract original for later verification + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + user_dict = pickled[TEST_USER_DN] + changes = {"full_name": "Renamed User"} + removes = [] + result = edit_user( + TEST_USER_DN, changes, removes, self.configuration, + keyword_auto + ) -class TestMigSharedUseradm__edit_user( + new_dn = result["distinguished_name"] + new_dir = new_dn.replace("/", "+").replace(" ", "_") + + pickled = self.assertPickledFile( + self.expected_user_db_file, + apply_hints=["convert_dict_bytes_to_strings_kv"], + ) + edited_user = pickled[new_dn] + self.assertNotIn(TEST_USER_DN, pickled) + self.assertIn(new_dn, pickled) + for field in user_dict: + if field == "full_name": + self.assertEqual(edited_user[field], "Renamed User") + elif field == "distinguished_name": + self.assertEqual(edited_user[field], new_dn) + else: + self.assertEqual(edited_user[field], user_dict[field]) + + old_home = os.path.join(self.configuration.user_home, TEST_USER_DIR) + new_home = os.path.join(self.configuration.user_home, new_dir) + self.assertFalse(os.path.exists(old_home)) + self.assertTrue(os.path.isdir(new_home)) + + old_settings = os.path.join( + self.configuration.user_settings, TEST_USER_DIR) + new_settings = os.path.join(self.configuration.user_settings, new_dir) + self.assertFalse(os.path.exists(old_settings)) + self.assertTrue(os.path.isdir(new_settings)) + + self._flush_test_user(new_dn) + + +class TestMigSharedUseradm__edit_user_uuid_user_id( MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin ): - """Coverage of useradm edit_user function.""" + """Coverage of useradm edit_user function with UUID format.""" TEST_USER_PASSWORD_HASH = ( "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM2134uix" @@ -1458,7 +1381,7 @@ class TestMigSharedUseradm__edit_user( def before_each(self): configuration = self.configuration - configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT + configuration.site_user_id_format = UUID_USER_ID_FORMAT _ensure_dirs_needed_for_userdb(self.configuration) @@ -1487,8 +1410,8 @@ def _flush_test_user(self, client_id): pass def test_edit_user_email(self): - """Test basic user email editing""" - self._provision_test_user(self, TEST_USER_DN) + """Test basic user email attribute editing with UUID""" + _provision_uuid_test_user(self.configuration, TEST_USER_DN) # Extract original for later verification pickled = self.assertPickledFile( self.expected_user_db_file, @@ -1520,11 +1443,11 @@ def test_edit_user_email(self): else: self.assertEqual(edited_user[field], user_dict[field]) - self._flush_test_user(edited_user['distinguished_name']) + self._flush_test_user(edited_dn) def test_edit_user_remove_attributes(self): - """Test removing user attributes""" - self._provision_test_user(self, TEST_USER_DN) + """Test removing user attributes with UUID""" + _provision_uuid_test_user(self.configuration, TEST_USER_DN) # Extract original for later verification pickled = self.assertPickledFile( self.expected_user_db_file, @@ -1553,8 +1476,8 @@ def test_edit_user_remove_attributes(self): self._flush_test_user(TEST_USER_DN) def test_edit_user_meta_only(self): - """Test metadata-only update (no FS changes)""" - self._provision_test_user(self, TEST_USER_DN) + """Test metadata-only update with UUID (no FS changes)""" + _provision_uuid_test_user(self.configuration, TEST_USER_DN) # Extract original for later verification pickled = self.assertPickledFile( self.expected_user_db_file, @@ -1576,7 +1499,7 @@ def test_edit_user_meta_only(self): edited_user = pickled[TEST_USER_DN] for field in user_dict: if field == "comment": - self.assertEqual(pickled[TEST_USER_DN][field], "meta comment") + self.assertEqual(edited_user[field], "meta comment") else: self.assertEqual(edited_user[field], user_dict[field]) @@ -1595,7 +1518,7 @@ def test_edit_user_nonexistent(self): @unittest.skip("make backend function less noisy (traceback) and enable?") def test_edit_user_nonexistent_force(self): - """Test editing nonexistent user with force=True""" + """Test editing nonexistent user with force=True and UUID""" changes = {"email": "dummy@example.com"} removes = [] # NOTE: forced edit logs errors in update_account_X functions @@ -1608,34 +1531,15 @@ def test_edit_user_nonexistent_force(self): @unittest.skip("make backend function less noisy (traceback) and enable?") def test_edit_user_dn_collision_fails(self): - """Test that editing a user to a DN that already exists fails""" - # NOTE: we can't use _provision_test_users here as it lacks passwords - test_user_dict = distinguished_name_to_user(TEST_USER_DN) - test_user_dict["comment"] = "This is the create comment" - test_user_dict["password"] = "" - test_user_dict["password_hash"] = self.TEST_USER_PASSWORD_HASH - try: - create_user( - test_user_dict, self.configuration, keyword_auto, default_renew=True - ) - except Exception: - self.assertFalse(True, "should not be reached") - other_user_dict = distinguished_name_to_user(OTHER_USER_DN) - other_user_dict["comment"] = "This is the create comment" - other_user_dict["password"] = "" - other_user_dict["password_hash"] = DUMMY_PASSWORD_HASH - try: - create_user( - other_user_dict, self.configuration, keyword_auto, default_renew=True - ) - except Exception: - self.assertFalse(True, "should not be reached") + """Test that editing a user to a DN that already exists fails with UUID""" + _provision_uuid_test_user(self.configuration, TEST_USER_DN) + _provision_uuid_test_user(self.configuration, OTHER_USER_DN) - changes = distinguished_name_to_user(OTHER_USER_DN) + changes = distinguished_name_to_user(TEST_USER_DN) removes = [] with self.assertRaises(Exception): edit_user( - TEST_USER_DN, changes, removes, self.configuration, + OTHER_USER_DN, changes, removes, self.configuration, keyword_auto ) @@ -1643,8 +1547,8 @@ def test_edit_user_dn_collision_fails(self): self._flush_test_user(OTHER_USER_DN) def test_edit_user_renames_user(self): - """Test editing fields that change the distinguished name""" - self._provision_test_user(self, TEST_USER_DN) + """Test editing fields that change the distinguished name with UUID""" + _provision_uuid_test_user(self.configuration, TEST_USER_DN) # Extract original for later verification pickled = self.assertPickledFile( self.expected_user_db_file, @@ -1660,7 +1564,7 @@ def test_edit_user_renames_user(self): ) new_dn = result["distinguished_name"] - new_dir = new_dn.replace("/", "+").replace(" ", "_") + new_link_dir = new_dn.replace("/", "+").replace(" ", "_") pickled = self.assertPickledFile( self.expected_user_db_file, @@ -1677,16 +1581,16 @@ def test_edit_user_renames_user(self): else: self.assertEqual(edited_user[field], user_dict[field]) - old_home = os.path.join(self.configuration.user_home, TEST_USER_DIR) - new_home = os.path.join(self.configuration.user_home, new_dir) - self.assertFalse(os.path.exists(old_home)) - self.assertTrue(os.path.isdir(new_home)) - - old_settings = os.path.join( - self.configuration.user_settings, TEST_USER_DIR) - new_settings = os.path.join(self.configuration.user_settings, new_dir) - self.assertFalse(os.path.exists(old_settings)) - self.assertTrue(os.path.isdir(new_settings)) + old_link = os.path.join( + self.configuration.user_home, TEST_USER_DIR) + new_link = os.path.join(self.configuration.user_home, new_link_dir) + real_home = os.path.join( + self.configuration.user_home, user_dict['unique_id']) + self.assertFalse(os.path.exists(old_link)) + self.assertTrue(os.path.islink(new_link)) + self.assertEqual(os.path.realpath(new_link), + os.path.realpath(real_home)) + self.assertTrue(os.path.isdir(real_home)) self._flush_test_user(new_dn) @@ -1768,6 +1672,100 @@ def test_get_required_user_alias_links_without_links(self): self.assertEqual(alias_links, [(False, False)]) +class TestMigSharedUseradm__lookup_client_id_from_uuid(MigTestCase): + """Coverage of useradm lookup_client_id_from_uuid function.""" + + def before_each(self): + """Create test environment for useradm tests""" + configuration = self.configuration + configuration.site_user_id_format = UUID_USER_ID_FORMAT + + _ensure_dirs_needed_for_userdb(self.configuration) + + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def _provide_configuration(self): + return "testconfig" + + def test_lookup_client_id_from_uuid_via_alias_link(self): + """Test lookup via existing alias link in mig_system_run""" + configuration = self.configuration + client_id = TEST_USER_DN + user_id = TEST_USER_UUID + # Setup real directory for the client and alias link in mig_system_run + real_dir = os.path.join(configuration.user_home, user_id) + ensure_dirs_exist(real_dir) + alias_link = os.path.join( + configuration.mig_system_run, user_id_alias_dir, user_id) + ensure_dirs_exist(os.path.dirname(alias_link)) + client_dir = client_id_dir(client_id) + os.symlink(client_dir, alias_link) + + result = lookup_client_id_from_uuid(configuration, user_id) + self.assertEqual(result, client_id) + + def test_lookup_client_id_from_uuid_via_reverse_lookup(self): + """Test lookup via reverse lookup in user_home (and writeback)""" + configuration = self.configuration + client_id = TEST_USER_DN + user_id = TEST_USER_UUID + # Setup real directory for the client and alias link in home + real_dir = os.path.join(configuration.user_home, user_id) + ensure_dirs_exist(real_dir) + # This simulates an existing X509 alias link to uuid in user_home + client_dir = client_id_dir(client_id) + alias_link_in_home = os.path.join(configuration.user_home, client_dir) + os.symlink(user_id, alias_link_in_home) + + result = lookup_client_id_from_uuid(configuration, user_id) + self.assertEqual(result, client_id) + + # Verify writeback: check if alias link was created in mig_system_run + alias_link_in_run = os.path.join(configuration.mig_system_run, + user_id_alias_dir, user_id) + self.assertTrue(os.path.islink(alias_link_in_run)) + link_target = os.path.basename(os.path.realpath(alias_link_in_run)) + self.assertEqual(link_target, client_dir) + + def test_lookup_client_id_from_uuid_fails_with_only_short_id_link(self): + """Test lookup via reverse lookup in user_home fails on short id link""" + configuration = self.configuration + client_id = TEST_USER_DN + user_id = TEST_USER_UUID + # Setup real directory for the client and alias link in home + real_dir = os.path.join(configuration.user_home, user_id) + ensure_dirs_exist(real_dir) + # This simulates an existing short id link to uuid in user_home + short_id_link_in_home = os.path.join(configuration.user_home, + TEST_USER_EMAIL) + os.symlink(user_id, short_id_link_in_home) + + with self.assertLogs(level='ERROR') as log_capture: + result = lookup_client_id_from_uuid(configuration, user_id) + self.assertNotEqual(result, client_id) + self.assertEqual(result, user_id) + self.assertTrue(any('found no alias' in msg + for msg in log_capture.output)) + + def test_lookup_client_id_from_uuid_not_found(self): + """Test lookup when no alias or reverse link exists""" + configuration = self.configuration + client_id = NO_SUCH_USER_DN + # Missing user will cause log error + with self.assertLogs(level='ERROR') as log_capture: + result = lookup_client_id_from_uuid(configuration, client_id) + # Verify: should return the user_id itself if not found + self.assertEqual(result, client_id) + self.assertTrue(any('found no alias' in msg + for msg in log_capture.output)) + + class TestMigSharedUseradm__get_any_oid_user_dn( MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin ): @@ -1853,5 +1851,146 @@ def test_get_any_oid_user_dn_user_check_false(self): ) self.assertEqual(result, raw_login) +# TODO: consider merging the tests for UUID and X509 format + + +class TestMigSharedUseradm__get_any_oid_user_dn_uuid_user_id( + MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin +): + """Unit tests for get_any_oid_user_dn with UUID user ID format.""" + + def before_each(self): + """Prepare a minimal configuration for the tests.""" + configuration = self.configuration + # Use UUID format for the tests – the function works with both UUID and X509. + configuration.site_user_id_format = UUID_USER_ID_FORMAT + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def _provide_configuration(self): + return "testconfig" + + def _flush_test_user(self, client_id): + """Helper to force clean up after provisioned test users""" + try: + delete_user( + {'distinguished_name': client_id}, + self.configuration, + keyword_auto, + force=True, + ) + except Exception: + pass + + def test_get_any_oid_user_dn_via_lookup_link(self): + """Return the distinguished name when a valid lookup link exists.""" + client_id = TEST_USER_DN + user_dict = _provision_uuid_test_user(self.configuration, client_id) + user_id = user_dict['unique_id'] + short_id = user_dict['short_id'] + + # Make sure direct lookup link is in place + lookup_link = os.path.join(self.configuration.mig_system_run, + user_id_alias_dir, user_id) + self.assertTrue(os.path.islink(lookup_link)) + + # Call the function – it should resolve the lookup to the client_id. + result = get_any_oid_user_dn(self.configuration, raw_login=short_id, + user_check=True, do_lock=True + ) + self._flush_test_user(TEST_USER_DN) + self.assertEqual(result, client_id) + + def test_get_any_oid_user_dn_via_fallback_id_link(self): + """Return the distinguished name when a valid fallback ID link exists.""" + client_id = TEST_USER_DN + user_dict = _provision_uuid_test_user(self.configuration, client_id) + user_id = user_dict['unique_id'] + short_id = user_dict['short_id'] + client_dir = client_id_dir(client_id) + + # Blow away direct lookup link to force alias lookup + lookup_link = os.path.join(self.configuration.mig_system_run, + user_id_alias_dir, user_id) + if os.path.islink(lookup_link): + os.remove(lookup_link) + self.assertFalse(os.path.islink(lookup_link)) + + # Make sure short alias, DN link and id dir are all in place + alias_link = os.path.join(self.configuration.user_home, short_id) + self.assertTrue(os.path.islink(alias_link)) + self.assertEqual(os.path.basename(os.path.realpath(alias_link)), + user_id) + dn_link = os.path.join(self.configuration.user_home, client_dir) + self.assertTrue(os.path.islink(dn_link)) + self.assertEqual(os.path.basename(os.path.realpath(dn_link)), + user_id) + user_dir = os.path.join(self.configuration.user_home, user_id) + self.assertTrue(os.path.isdir(user_dir)) + + # Call the function – it should resolve the alias to the client_id. + result = get_any_oid_user_dn(self.configuration, raw_login=short_id, + user_check=True, do_lock=True + ) + self._flush_test_user(TEST_USER_DN) + self.assertEqual(result, client_id) + + def test_get_any_oid_user_dn_not_found(self): + """When no alias or reverse link exists, return an empty string.""" + # Missing user will cause log error + with self.assertLogs(level='ERROR') as log_capture: + result = get_any_oid_user_dn(self.configuration, + raw_login="NoSuchUser", + user_check=True, do_lock=True + ) + self.assertEqual(result, "") + self.assertTrue(any('no such openid user' in msg + for msg in log_capture.output)) + + def test_get_any_oid_user_id_direct_dn(self): + """Return the distinguished name when a matching id directory exists.""" + client_id = TEST_USER_DN + user_dict = _provision_uuid_test_user(self.configuration, client_id) + user_id = user_dict['unique_id'] + short_id = user_dict['short_id'] + client_dir = client_id_dir(client_id) + + # Make sure no lookups links get in the way and that id dir is in place + lookup_link = os.path.join(self.configuration.mig_system_run, + user_id_alias_dir, user_id) + os.remove(lookup_link) + self.assertFalse(os.path.islink(lookup_link)) + alias_link = os.path.join(self.configuration.user_home, short_id) + os.remove(alias_link) + self.assertFalse(os.path.islink(alias_link)) + reverse_link = os.path.join(self.configuration.user_home, client_dir) + os.remove(reverse_link) + self.assertFalse(os.path.islink(reverse_link)) + user_dir = os.path.join(self.configuration.user_home, user_id) + self.assertTrue(os.path.isdir(user_dir)) + + # The function should recognise the directory and return the client_id. + result = get_any_oid_user_dn(self.configuration, + raw_login=user_id, + user_check=True, do_lock=True + ) + self._flush_test_user(TEST_USER_DN) + self.assertEqual(result, user_id) + + def test_get_any_oid_user_dn_user_check_false(self): + """When user_check=False the function bypasses the user‑dir lookup.""" + raw_login = TEST_USER_SHORT_ID + result = get_any_oid_user_dn(self.configuration, raw_login=raw_login, + user_check=False, do_lock=True + ) + self.assertEqual(result, raw_login) + + if __name__ == "__main__": testmain() From c944f7e8c113281828e2a954f0b8d7e1dac0cd12 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 1 Jun 2026 14:52:10 +0200 Subject: [PATCH 09/61] Drop unused import of `lookup_client_id` helper. May need reintroduction later. --- mig/shared/griddaemons/login.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mig/shared/griddaemons/login.py b/mig/shared/griddaemons/login.py index 4caf98413..83218300c 100644 --- a/mig/shared/griddaemons/login.py +++ b/mig/shared/griddaemons/login.py @@ -4,7 +4,7 @@ # --- BEGIN_HEADER --- # # login - grid daemon login helper functions -# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH +# Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH # # This file is part of MiG. # @@ -47,8 +47,7 @@ https_authkeys, get_authkeys, ssh_authpasswords, davs_authpasswords, \ ftps_authpasswords, https_authpasswords, get_authpasswords, \ ssh_authdigests, davs_authdigests, ftps_authdigests, https_authdigests, \ - generate_password_hash, generate_password_digest, load_user_dict, \ - lookup_client_id + generate_password_hash, generate_password_digest, load_user_dict from mig.shared.validstring import possible_sharelink_id, possible_job_id, \ possible_jupyter_mount_id From 1d3a7b9fb00b814451b78ed3465e3fcb809471ab Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 1 Jun 2026 14:54:32 +0200 Subject: [PATCH 10/61] Non-functional change to clarify the role and use of `lookup_client_id` to be specific to the UUID user ID format and elaborate what it does in the function docstring. --- mig/shared/useradm.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mig/shared/useradm.py b/mig/shared/useradm.py index 44549c632..388dc6d49 100644 --- a/mig/shared/useradm.py +++ b/mig/shared/useradm.py @@ -284,9 +284,11 @@ def _get_required_user_alias_links(configuration, real_dir, link_dir): return alias_links -def lookup_client_id(configuration, user_id): - """Lookup the X509 format client_id linked to user_id based on the helper - symlink in user_id_alias_dir in configured mig_system_run location. +def lookup_client_id_from_uuid(configuration, user_id): + """Lookup the X509 format client_id linked to UUID user_id based on the + helper symlink in user_id_alias_dir in configured mig_system_run location. + Automatic fallback to reverse lookup based on symlinks in user_home if + helper symlink is missing with write-through to mig_system_run cache. """ _logger = configuration.logger alias_link = os.path.join(configuration.mig_system_run, user_id_alias_dir, @@ -312,6 +314,7 @@ def lookup_client_id(configuration, user_id): makedirs_rec(os.path.dirname(alias_link), configuration) make_symlink(client_dir, alias_link, _logger) break + if not client_dir: _logger.warning("found no alias %r" % user_id) return user_id @@ -1875,7 +1878,8 @@ def get_any_oid_user_dn(configuration, raw_login, (native_dir, raw_login)) if configuration.site_user_id_format == UUID_USER_ID_FORMAT: user_id = native_dir - distinguished_name = lookup_client_id(configuration, user_id) + distinguished_name = lookup_client_id_from_uuid(configuration, + user_id) elif configuration.site_user_id_format == X509_USER_ID_FORMAT: distinguished_name = client_dir_id(native_dir) else: From d4f025063d1f755614480638ec8d665291f564c0 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 1 Jun 2026 14:57:43 +0200 Subject: [PATCH 11/61] Add unit tests for the recently renamed `lookup_client_id_from_uuid` helper used in UUID user ID format setups. --- tests/test_mig_shared_useradm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 08ed830c9..5ac4506ab 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -62,6 +62,7 @@ delete_user, edit_user, get_any_oid_user_dn, + lookup_client_id_from_uuid, user_account_notify, ) @@ -1991,6 +1992,5 @@ def test_get_any_oid_user_dn_user_check_false(self): ) self.assertEqual(result, raw_login) - if __name__ == "__main__": testmain() From 51f9ac2c20d941aac8fe161ca632a840c9ad1c49 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 1 Jun 2026 16:51:31 +0200 Subject: [PATCH 12/61] Add unit tests for get_any_oid_user_dn in the case of UUID user ID format. --- tests/test_mig_shared_useradm.py | 95 ++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 5ac4506ab..35bf5cb96 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -1992,5 +1992,100 @@ def test_get_any_oid_user_dn_user_check_false(self): ) self.assertEqual(result, raw_login) +class TestMigSharedUseradm__get_any_oid_user_dn_uuid_user_id( + MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin +): + """Unit tests for get_any_oid_user_dn with UUID user ID format.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Prepare a minimal configuration for the tests.""" + configuration = self.configuration + # Use UUID format for the tests – the function works with both UUID and X509. + configuration.site_user_id_format = UUID_USER_ID_FORMAT + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def test_get_any_oid_user_dn_via_alias_link(self): + """Return the distinguished name when a valid alias link exists.""" + client_id = TEST_USER_DN + user_dict = _provision_uuid_test_user(self.configuration, client_id) + user_id = user_dict['unique_id'] + short_id = user_dict['short_id'] + + # Make sure direct lookup link is in place + lookup_link = os.path.join(self.configuration. mig_system_run, + user_id_alias_dir, user_id) + self.assertTrue(os.path.islink(lookup_link)) + + # Call the function – it should resolve the alias to the client_id. + result = get_any_oid_user_dn(self.configuration, raw_login=short_id, + user_check=True, do_lock=True + ) + self.assertEqual(result, client_id) + + def test_get_any_oid_user_dn_via_reverse_lookup(self): + """Return the distinguished name when a reverse‑lookup symlink exists.""" + client_id = TEST_USER_DN + user_dict = _provision_uuid_test_user(self.configuration, client_id) + user_id = user_dict['unique_id'] + short_id = user_dict['short_id'] + + # Blow away direct lookup link to force reverse lookup + lookup_link = os.path.join(self.configuration. mig_system_run, + user_id_alias_dir, user_id) + if os.path.islink(lookup_link): + os.remove(lookup_link) + self.assertFalse(os.path.islink(lookup_link)) + + # The function should find the client_id via the reverse lookup. + result = get_any_oid_user_dn(self.configuration, raw_login=short_id, + user_check=True, do_lock=True + ) + self.assertEqual(result, client_id) + + def test_get_any_oid_user_dn_not_found(self): + """When no alias or reverse link exists, return an empty string.""" + # Missing user will cause log error + self.logger.forgive_errors() + result = get_any_oid_user_dn(self.configuration, + raw_login="NoSuchUser", + user_check=True, do_lock=True + ) + self.assertEqual(result, "") + + def test_get_any_oid_user_dn_direct_dn(self): + """Return the distinguished name when a matching cert directory exists.""" + user_id = TEST_USER_UUID # any valid client_id works for this test + + # Create a cert directory user_home/ that the function + # can see. + home_dir = os.path.join(self.configuration.user_home, user_id) + ensure_dirs_exist(home_dir) + + # The function should recognise the directory and return the client_id. + result = get_any_oid_user_dn(self.configuration, + raw_login=TEST_USER_UUID, + user_check=True, do_lock=True + ) + self.assertEqual(result, user_id) + + def test_get_any_oid_user_dn_user_check_false(self): + """When user_check=False the function bypasses the user‑dir lookup.""" + raw_login = TEST_USER_SHORT_ID + result = get_any_oid_user_dn(self.configuration, raw_login=raw_login, + user_check=False, do_lock=True + ) + self.assertEqual(result, raw_login) + + if __name__ == "__main__": testmain() From ef5c47ba99a63e9a1656269509b31510c2aaac22 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 1 Jun 2026 17:03:14 +0200 Subject: [PATCH 13/61] Add similar unit tests for get_any_oid_user_dn with the default user ID format. --- tests/test_mig_shared_useradm.py | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 35bf5cb96..3d4449666 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -1992,6 +1992,78 @@ def test_get_any_oid_user_dn_user_check_false(self): ) self.assertEqual(result, raw_login) +class TestMigSharedUseradm__get_any_oid_user_dn( + MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin +): + """Unit tests for get_any_oid_user_dn with default user ID format.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Prepare a minimal configuration for the tests.""" + configuration = self.configuration + # Use Default format for these tests + configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + + def test_get_any_oid_user_dn_via_alias_link(self): + """Return the distinguished name when a valid alias link exists.""" + client_id = TEST_USER_DN + short_id = TEST_USER_EMAIL + self._provision_test_user(self, client_id) + + # Make sure alias link is in place + alias_link = os.path.join(self.configuration.user_home, short_id) + client_dir = client_id_dir(client_id) + os.symlink(client_dir, alias_link) + + # Call the function – it should resolve the alias to the client_id. + result = get_any_oid_user_dn(self.configuration, raw_login=short_id, + user_check=True, do_lock=True + ) + self.assertEqual(result, client_id) + + def test_get_any_oid_user_dn_not_found(self): + """When no alias or reverse link exists, return an empty string.""" + # Missing user will cause log error + self.logger.forgive_errors() + result = get_any_oid_user_dn(self.configuration, + raw_login="NoSuchUser", + user_check=True, do_lock=True + ) + self.assertEqual(result, "") + + def test_get_any_oid_user_dn_direct_dn(self): + """Return the distinguished name when a matching cert directory exists.""" + client_id = TEST_USER_DN + self._provision_test_user(self, client_id) + + # The function should recognise the directory and return the client_id. + result = get_any_oid_user_dn(self.configuration, + raw_login=TEST_USER_DN, + user_check=True, do_lock=True + ) + self.assertEqual(result, client_id) + + def test_get_any_oid_user_dn_user_check_false(self): + """When user_check=False the function bypasses the user‑dir lookup.""" + raw_login = TEST_USER_SHORT_ID + result = get_any_oid_user_dn(self.configuration, raw_login=raw_login, + user_check=False, do_lock=True + ) + self.assertEqual(result, raw_login) + +# TODO: consider merging the tests for UUID and X509 format + + class TestMigSharedUseradm__get_any_oid_user_dn_uuid_user_id( MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin ): @@ -2022,7 +2094,7 @@ def test_get_any_oid_user_dn_via_alias_link(self): short_id = user_dict['short_id'] # Make sure direct lookup link is in place - lookup_link = os.path.join(self.configuration. mig_system_run, + lookup_link = os.path.join(self.configuration.mig_system_run, user_id_alias_dir, user_id) self.assertTrue(os.path.islink(lookup_link)) @@ -2040,7 +2112,7 @@ def test_get_any_oid_user_dn_via_reverse_lookup(self): short_id = user_dict['short_id'] # Blow away direct lookup link to force reverse lookup - lookup_link = os.path.join(self.configuration. mig_system_run, + lookup_link = os.path.join(self.configuration.mig_system_run, user_id_alias_dir, user_id) if os.path.islink(lookup_link): os.remove(lookup_link) @@ -2064,7 +2136,7 @@ def test_get_any_oid_user_dn_not_found(self): def test_get_any_oid_user_dn_direct_dn(self): """Return the distinguished name when a matching cert directory exists.""" - user_id = TEST_USER_UUID # any valid client_id works for this test + user_id = TEST_USER_UUID # Create a cert directory user_home/ that the function # can see. From 45c366ef2d3f8e955eab47c02da4c2c865765dc2 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 1 Jun 2026 17:10:52 +0200 Subject: [PATCH 14/61] A bit of consistency clean up and addition of flush user as clean up after tests where users are provisioned to avoid cross-talk between tests. --- tests/test_mig_shared_useradm.py | 40 +++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 3d4449666..f173f3407 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -1997,9 +1997,6 @@ class TestMigSharedUseradm__get_any_oid_user_dn( ): """Unit tests for get_any_oid_user_dn with default user ID format.""" - def _provide_configuration(self): - return "testconfig" - def before_each(self): """Prepare a minimal configuration for the tests.""" configuration = self.configuration @@ -2014,6 +2011,21 @@ def before_each(self): ) ensure_dirs_exist(self.configuration.mig_system_files) + def _provide_configuration(self): + return "testconfig" + + def _flush_test_user(self, client_id): + """Helper to force clean up after provisioned test users""" + try: + delete_user( + {'distinguished_name': client_id}, + self.configuration, + keyword_auto, + force=True, + ) + except Exception: + pass + def test_get_any_oid_user_dn_via_alias_link(self): """Return the distinguished name when a valid alias link exists.""" client_id = TEST_USER_DN @@ -2030,6 +2042,7 @@ def test_get_any_oid_user_dn_via_alias_link(self): user_check=True, do_lock=True ) self.assertEqual(result, client_id) + self._flush_test_user(TEST_USER_DN) def test_get_any_oid_user_dn_not_found(self): """When no alias or reverse link exists, return an empty string.""" @@ -2052,6 +2065,7 @@ def test_get_any_oid_user_dn_direct_dn(self): user_check=True, do_lock=True ) self.assertEqual(result, client_id) + self._flush_test_user(TEST_USER_DN) def test_get_any_oid_user_dn_user_check_false(self): """When user_check=False the function bypasses the user‑dir lookup.""" @@ -2069,9 +2083,6 @@ class TestMigSharedUseradm__get_any_oid_user_dn_uuid_user_id( ): """Unit tests for get_any_oid_user_dn with UUID user ID format.""" - def _provide_configuration(self): - return "testconfig" - def before_each(self): """Prepare a minimal configuration for the tests.""" configuration = self.configuration @@ -2086,6 +2097,21 @@ def before_each(self): ) ensure_dirs_exist(self.configuration.mig_system_files) + def _provide_configuration(self): + return "testconfig" + + def _flush_test_user(self, client_id): + """Helper to force clean up after provisioned test users""" + try: + delete_user( + {'distinguished_name': client_id}, + self.configuration, + keyword_auto, + force=True, + ) + except Exception: + pass + def test_get_any_oid_user_dn_via_alias_link(self): """Return the distinguished name when a valid alias link exists.""" client_id = TEST_USER_DN @@ -2103,6 +2129,7 @@ def test_get_any_oid_user_dn_via_alias_link(self): user_check=True, do_lock=True ) self.assertEqual(result, client_id) + self._flush_test_user(TEST_USER_DN) def test_get_any_oid_user_dn_via_reverse_lookup(self): """Return the distinguished name when a reverse‑lookup symlink exists.""" @@ -2123,6 +2150,7 @@ def test_get_any_oid_user_dn_via_reverse_lookup(self): user_check=True, do_lock=True ) self.assertEqual(result, client_id) + self._flush_test_user(TEST_USER_DN) def test_get_any_oid_user_dn_not_found(self): """When no alias or reverse link exists, return an empty string.""" From 5ebefc7c351796bfc09d11bd5ff3666f845c1739 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 3 Jun 2026 10:16:22 +0200 Subject: [PATCH 15/61] Try harder to make sure that the required X509 symlink to UUID user home exists before testing reverse lookup in the useradm unit tests for get_any_oid_user_dn with UUID format. Oddly it succeeds in `make distclean test` locally but fails across the board on the corresponding GH Action. --- tests/test_mig_shared_useradm.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index f173f3407..2d7f22b9c 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -2137,6 +2137,7 @@ def test_get_any_oid_user_dn_via_reverse_lookup(self): user_dict = _provision_uuid_test_user(self.configuration, client_id) user_id = user_dict['unique_id'] short_id = user_dict['short_id'] + client_dir = client_id_dir(client_id) # Blow away direct lookup link to force reverse lookup lookup_link = os.path.join(self.configuration.mig_system_run, @@ -2145,6 +2146,13 @@ def test_get_any_oid_user_dn_via_reverse_lookup(self): os.remove(lookup_link) self.assertFalse(os.path.islink(lookup_link)) + # Verify that reverse lookup link exists (provision should add it) + reverse_link = os.path.join(self.configuration.user_home, client_dir) + if not os.path.islink(reverse_link): + print("Warning: provision didn't create X509 symlink to UUID home") + os.symlink(user_id, reverse_link) + self.assertTrue(os.path.islink(reverse_link)) + # The function should find the client_id via the reverse lookup. result = get_any_oid_user_dn(self.configuration, raw_login=short_id, user_check=True, do_lock=True From e5effe6fab24575e0d24e77177a255ba137b8dac Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 3 Jun 2026 17:45:58 +0200 Subject: [PATCH 16/61] Further test fixes and adjustments to make them pass. --- tests/test_mig_shared_useradm.py | 53 +++++++++++++++++++------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 2d7f22b9c..62ebebd65 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -2112,8 +2112,8 @@ def _flush_test_user(self, client_id): except Exception: pass - def test_get_any_oid_user_dn_via_alias_link(self): - """Return the distinguished name when a valid alias link exists.""" + def test_get_any_oid_user_dn_via_lookup_link(self): + """Return the distinguished name when a valid lookup link exists.""" client_id = TEST_USER_DN user_dict = _provision_uuid_test_user(self.configuration, client_id) user_id = user_dict['unique_id'] @@ -2124,44 +2124,41 @@ def test_get_any_oid_user_dn_via_alias_link(self): user_id_alias_dir, user_id) self.assertTrue(os.path.islink(lookup_link)) - # Call the function – it should resolve the alias to the client_id. + # Call the function – it should resolve the lookup to the client_id. result = get_any_oid_user_dn(self.configuration, raw_login=short_id, user_check=True, do_lock=True ) - self.assertEqual(result, client_id) self._flush_test_user(TEST_USER_DN) + self.assertEqual(result, client_id) - def test_get_any_oid_user_dn_via_reverse_lookup(self): - """Return the distinguished name when a reverse‑lookup symlink exists.""" + def test_get_any_oid_user_dn_via_alias_link(self): + """Return the distinguished name when a valid alias link exists.""" client_id = TEST_USER_DN user_dict = _provision_uuid_test_user(self.configuration, client_id) user_id = user_dict['unique_id'] short_id = user_dict['short_id'] - client_dir = client_id_dir(client_id) - # Blow away direct lookup link to force reverse lookup + # Blow away direct lookup link to force alias lookup lookup_link = os.path.join(self.configuration.mig_system_run, user_id_alias_dir, user_id) if os.path.islink(lookup_link): os.remove(lookup_link) self.assertFalse(os.path.islink(lookup_link)) - # Verify that reverse lookup link exists (provision should add it) - reverse_link = os.path.join(self.configuration.user_home, client_dir) - if not os.path.islink(reverse_link): - print("Warning: provision didn't create X509 symlink to UUID home") - os.symlink(user_id, reverse_link) - self.assertTrue(os.path.islink(reverse_link)) + # Make sure alias link is in place + alias_link = os.path.join(self.configuration.user_home, short_id) + self.assertTrue(os.path.islink(alias_link)) - # The function should find the client_id via the reverse lookup. + # Call the function – it should resolve the alias to the client_id. result = get_any_oid_user_dn(self.configuration, raw_login=short_id, user_check=True, do_lock=True ) - self.assertEqual(result, client_id) self._flush_test_user(TEST_USER_DN) + self.assertEqual(result, client_id) def test_get_any_oid_user_dn_not_found(self): """When no alias or reverse link exists, return an empty string.""" + # TODO: can we fix or avoid this log ignore? # Missing user will cause log error self.logger.forgive_errors() result = get_any_oid_user_dn(self.configuration, @@ -2172,18 +2169,30 @@ def test_get_any_oid_user_dn_not_found(self): def test_get_any_oid_user_dn_direct_dn(self): """Return the distinguished name when a matching cert directory exists.""" - user_id = TEST_USER_UUID + client_id = TEST_USER_DN + user_dict = _provision_uuid_test_user(self.configuration, client_id) + user_id = user_dict['unique_id'] + short_id = user_dict['short_id'] + client_dir = client_id_dir(client_id) - # Create a cert directory user_home/ that the function - # can see. - home_dir = os.path.join(self.configuration.user_home, user_id) - ensure_dirs_exist(home_dir) + # Make sure no lookups links get in the way + lookup_link = os.path.join(self.configuration.mig_system_run, + user_id_alias_dir, user_id) + os.remove(lookup_link) + self.assertFalse(os.path.islink(lookup_link)) + alias_link = os.path.join(self.configuration.user_home, short_id) + os.remove(alias_link) + self.assertFalse(os.path.islink(alias_link)) + reverse_link = os.path.join(self.configuration.user_home, client_dir) + os.remove(reverse_link) + self.assertFalse(os.path.islink(reverse_link)) # The function should recognise the directory and return the client_id. result = get_any_oid_user_dn(self.configuration, - raw_login=TEST_USER_UUID, + raw_login=user_id, user_check=True, do_lock=True ) + self._flush_test_user(TEST_USER_DN) self.assertEqual(result, user_id) def test_get_any_oid_user_dn_user_check_false(self): From 4f7bdf2e30cfdc206ebe8505577183e032e14031 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 9 Jun 2026 13:16:00 +0200 Subject: [PATCH 17/61] Adjust warning to error and make unit tests more robust with actual error log checks and pre-run checks to pinpoint any remaining issues on GH runners. --- mig/shared/useradm.py | 2 +- tests/test_mig_shared_useradm.py | 52 ++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/mig/shared/useradm.py b/mig/shared/useradm.py index 388dc6d49..c56493e06 100644 --- a/mig/shared/useradm.py +++ b/mig/shared/useradm.py @@ -316,7 +316,7 @@ def lookup_client_id_from_uuid(configuration, user_id): break if not client_dir: - _logger.warning("found no alias %r" % user_id) + _logger.error("found no alias %r" % user_id) return user_id client_id = client_dir_id(client_dir) return client_id diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 62ebebd65..5df01d97d 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -2047,12 +2047,14 @@ def test_get_any_oid_user_dn_via_alias_link(self): def test_get_any_oid_user_dn_not_found(self): """When no alias or reverse link exists, return an empty string.""" # Missing user will cause log error - self.logger.forgive_errors() - result = get_any_oid_user_dn(self.configuration, - raw_login="NoSuchUser", - user_check=True, do_lock=True - ) - self.assertEqual(result, "") + with self.assertLogs(level='ERROR') as log_capture: + result = get_any_oid_user_dn(self.configuration, + raw_login="NoSuchUser", + user_check=True, do_lock=True + ) + self.assertEqual(result, "") + self.assertTrue(any('no such openid user' in msg + for msg in log_capture.output)) def test_get_any_oid_user_dn_direct_dn(self): """Return the distinguished name when a matching cert directory exists.""" @@ -2131,12 +2133,13 @@ def test_get_any_oid_user_dn_via_lookup_link(self): self._flush_test_user(TEST_USER_DN) self.assertEqual(result, client_id) - def test_get_any_oid_user_dn_via_alias_link(self): - """Return the distinguished name when a valid alias link exists.""" + def test_get_any_oid_user_dn_via_fallback_id_link(self): + """Return the distinguished name when a valid fallback ID link exists.""" client_id = TEST_USER_DN user_dict = _provision_uuid_test_user(self.configuration, client_id) user_id = user_dict['unique_id'] short_id = user_dict['short_id'] + client_dir = client_id_dir(client_id) # Blow away direct lookup link to force alias lookup lookup_link = os.path.join(self.configuration.mig_system_run, @@ -2145,9 +2148,17 @@ def test_get_any_oid_user_dn_via_alias_link(self): os.remove(lookup_link) self.assertFalse(os.path.islink(lookup_link)) - # Make sure alias link is in place + # Make sure short alias, DN link and id dir are all in place alias_link = os.path.join(self.configuration.user_home, short_id) self.assertTrue(os.path.islink(alias_link)) + self.assertEqual(os.path.basename(os.path.realpath(alias_link)), + user_id) + dn_link = os.path.join(self.configuration.user_home, client_dir) + self.assertTrue(os.path.islink(dn_link)) + self.assertEqual(os.path.basename(os.path.realpath(dn_link)), + user_id) + user_dir = os.path.join(self.configuration.user_home, user_id) + self.assertTrue(os.path.isdir(user_dir)) # Call the function – it should resolve the alias to the client_id. result = get_any_oid_user_dn(self.configuration, raw_login=short_id, @@ -2158,24 +2169,25 @@ def test_get_any_oid_user_dn_via_alias_link(self): def test_get_any_oid_user_dn_not_found(self): """When no alias or reverse link exists, return an empty string.""" - # TODO: can we fix or avoid this log ignore? # Missing user will cause log error - self.logger.forgive_errors() - result = get_any_oid_user_dn(self.configuration, - raw_login="NoSuchUser", - user_check=True, do_lock=True - ) - self.assertEqual(result, "") + with self.assertLogs(level='ERROR') as log_capture: + result = get_any_oid_user_dn(self.configuration, + raw_login="NoSuchUser", + user_check=True, do_lock=True + ) + self.assertEqual(result, "") + self.assertTrue(any('no such openid user' in msg + for msg in log_capture.output)) - def test_get_any_oid_user_dn_direct_dn(self): - """Return the distinguished name when a matching cert directory exists.""" + def test_get_any_oid_user_id_direct_dn(self): + """Return the distinguished name when a matching id directory exists.""" client_id = TEST_USER_DN user_dict = _provision_uuid_test_user(self.configuration, client_id) user_id = user_dict['unique_id'] short_id = user_dict['short_id'] client_dir = client_id_dir(client_id) - # Make sure no lookups links get in the way + # Make sure no lookups links get in the way and that id dir is in place lookup_link = os.path.join(self.configuration.mig_system_run, user_id_alias_dir, user_id) os.remove(lookup_link) @@ -2186,6 +2198,8 @@ def test_get_any_oid_user_dn_direct_dn(self): reverse_link = os.path.join(self.configuration.user_home, client_dir) os.remove(reverse_link) self.assertFalse(os.path.islink(reverse_link)) + user_dir = os.path.join(self.configuration.user_home, user_id) + self.assertTrue(os.path.isdir(user_dir)) # The function should recognise the directory and return the client_id. result = get_any_oid_user_dn(self.configuration, From de3f88f789b3d652caee7187b71cf41c86d2ce39 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 9 Jun 2026 14:57:29 +0200 Subject: [PATCH 18/61] Tighten the reverse lookup in lookup_client_id_from_uuid to prevent concluding that a short_id alias link is the DN link to use. --- mig/shared/useradm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mig/shared/useradm.py b/mig/shared/useradm.py index c56493e06..324013e9c 100644 --- a/mig/shared/useradm.py +++ b/mig/shared/useradm.py @@ -304,6 +304,9 @@ def lookup_client_id_from_uuid(configuration, user_id): # mig_system_run, which is only scratch space. client_dir = False for name in listdir(configuration.user_home): + # Skip if not a DN-dir name to avoid hit e.g. on short alias links + if '=' not in name or '+' not in name: + continue link_path = os.path.join(configuration.user_home, name) if os.path.islink(link_path): target = os.path.basename(os.path.realpath(link_path)) From eb4dd7f082104a6209b130d16f341b6fb84bf808 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 9 Jun 2026 15:15:08 +0200 Subject: [PATCH 19/61] Tighten edit_user tests to verify not just changes but also that nothing else changed. Similarly make sure delete_user is really called on the exact matching user from provision uuid user helper. Add a regression test to reflect the test errors we saw on GH before recently tightening the reverse lookup link filter in the lookup_client_id_from_uuid function. --- tests/test_mig_shared_useradm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 5df01d97d..a48d0e976 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -1052,11 +1052,11 @@ def _provide_configuration(self): return "testconfig" def test_user_delete_completes(self): - _provision_uuid_test_user(self.configuration, TEST_USER_DN) + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) try: delete_user( - distinguished_name_to_user(TEST_USER_DN), + user_dict, self.configuration, keyword_auto, force=True, @@ -1065,11 +1065,11 @@ def test_user_delete_completes(self): self.assertFalse(True, "should not be reached") def test_user_deletion_removes_fs_entries(self): - _provision_uuid_test_user(self.configuration, TEST_USER_DN) + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) try: delete_user( - distinguished_name_to_user(TEST_USER_DN), + user_dict, self.configuration, keyword_auto, force=True, From 66a3c94ead9bab0c766ead1e5245e959838d1766 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 10 Jun 2026 14:14:38 +0200 Subject: [PATCH 20/61] Establish griddaemons unit tests with just some simple login helper tests. --- tests/test_mig_shared_griddaemons.py | 209 +++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 tests/test_mig_shared_griddaemons.py diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py new file mode 100644 index 000000000..8d3d31fe6 --- /dev/null +++ b/tests/test_mig_shared_griddaemons.py @@ -0,0 +1,209 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# --- BEGIN_HEADER --- +# +# test_mig_shared_griddaemons - unit tests for the griddaemons login helpers +# Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH +# +# This file is part of MiG. +# +# MiG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# MiG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# --- END_HEADER --- +# + +"""Unit tests for the griddaemons login helper functions""" + +import os +import time +import unittest + +# Imports required for the unit test wrapping +from mig.shared.useradm import ( + _ensure_dirs_needed_for_userdb, +) + +# Imports of the code under test +from mig.shared.griddaemons.login import ( + Login, get_creds_changes, get_share_changes +) + +# Imports required for the unit tests themselves +from tests.support import ( + ensure_dirs_exist, + MigTestCase, temppath +) + +TEST_USER_SHORT_ID = "abc123@some.org" + +# NOTE: this is a sample valid but unused ssh public key as it must be parsable +TEST_USER_PUB_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuJrICshi7S2KhV03qvgNVOx5ejmHsswdGbvR34wf+eN23Ghq6OZhwGye2S+J6LPVFI3p4SCqxX4URnUM8BRAsiuvbf/+GQfE2pAO0C+/g4V3hhYbYzIyrtPsP1Hl8GioxvZD5nDoLEA4TWokDC4D7SRfv+NEkFLplyVBwHtpUunXBS/zXYdQ4lgk7u8HBBCMqUGbZHfCc+6ibFVn/5WS6vVokL+fSWtxg9tUVWqsS/xtDGPH1wbJUf1Dm3D58KmdX8ca73tBoScwH8qUQwEcyM1JtWtbv1BAZFb+Qk6SEe4GPRsn3I4AAgC7xtU3HKQsiqe8Fzpick/uI5PU+vguitcV/9+AASnGVZJ9M+a63UvlFFloEYcI1LwdZ03JYPQXfXCzJSYiA+pTX4/cf10G4rlxsque4m4OcuCwLKvpTWA/Lla+UJqYhdQW+m7mSizPRoDPgh8mOta1PQob2sGSw8rhqLfApptAPZ0mkN0QY3Dv3i3ItgpYGcPNVXVdjmhU=' +# NOTE: this is a sample valid but unused ssh password hash as it must be parsable +TEST_USER_PW_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM1234uxi" + + +class MigSharedGriddaemonsLogin(MigTestCase): + """Unit tests for griddaemons login helper functions""" + + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test""" + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['jobs'] = [] + + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False + + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + # self.auth_digests_path = temppath('authhorized_digests', self) + self.auth_digests_path = None + + # Create sample credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + # with open(self.auth_digests_path, 'w') as creds_fd: + # creds_fd.write(TEST_USER_DIGEST) + + def test_get_creds_changes_detects_new_files(self): + """Verify that new credential files are detected as changes""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True + daemon_conf['allow_password'] = True + + # Create a dummy user with a last_update in the past + past_timestamp = time.time() - 3600 + dummy_user = Login( + configuration=self.configuration, + username='dummy', + home='dummy_home', + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_user.last_update = past_timestamp + daemon_conf['users'].append(dummy_user) + + changed_paths = get_creds_changes( + daemon_conf, + 'user', + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path + ) + + self.assertIn(self.auth_keys_path, changed_paths) + self.assertIn(self.auth_passwords_path, changed_paths) + # self.assertIn(self.auth_digests_path, changed_paths) + + def test_get_creds_changes_no_changes(self): + """Verify that unchanged credential files return an empty list""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True + daemon_conf['allow_password'] = True + + # Set the file modification times to a time in the past + current_time = time.time() + past_timestamp = time.time() - 3600 + # os.utime(self.auth_keys_path, (past_timestamp, past_timestamp)) + # os.utime(self.auth_passwords_path, (past_timestamp, past_timestamp)) + # os.utime(self.auth_digests_path, (past_timestamp, past_timestamp)) + + # Create a dummy user with last_update matching the file mtime + dummy_user = Login( + configuration=self.configuration, + username=TEST_USER_SHORT_ID, + home='dummy_home', + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_user.last_update = current_time + daemon_conf['users'].append(dummy_user) + + changed_paths = get_creds_changes( + daemon_conf, + TEST_USER_SHORT_ID, + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path + ) + + self.assertEqual(len(changed_paths), 0) + + def test_get_share_changes_detects_updates(self): + """Verify that share link and key file changes are detected""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True + + share_id = 'Abcdef1234' + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') + ensure_dirs_exist(user_shared_dir) + user_shared_keys = os.path.join(user_shared_dir, '.ssh', + 'authorized_keys') + share_link_path = os.path.join(self.configuration.sharelink_home, + share_id) + os.symlink(user_shared_dir, share_link_path) + + # Create a dummy share with a last_update in the past + past_timestamp = time.time() - 3600 + dummy_share = Login( + configuration=self.configuration, + username=share_id, + home='share_home', + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_share.last_update = past_timestamp + daemon_conf['shares'].append(dummy_share) + + changed_paths = get_share_changes( + daemon_conf, + share_id, + share_link_path, + self.auth_keys_path + ) + + self.assertIn(share_link_path, changed_paths) + + +if __name__ == '__main__': + unittest.main() From 2b18634056fbcf9aa58b1be9f4e6fff5dea9c097 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 10 Jun 2026 14:20:50 +0200 Subject: [PATCH 21/61] Minor clean up. --- tests/test_mig_shared_griddaemons.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 8d3d31fe6..eb5ede706 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -3,7 +3,7 @@ # # --- BEGIN_HEADER --- # -# test_mig_shared_griddaemons - unit tests for the griddaemons login helpers +# test_mig_shared_griddaemons - unit tests for the griddaemons helpers # Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH # # This file is part of MiG. @@ -25,7 +25,7 @@ # --- END_HEADER --- # -"""Unit tests for the griddaemons login helper functions""" +"""Unit tests for the griddaemons helper functions""" import os import time @@ -48,6 +48,7 @@ ) TEST_USER_SHORT_ID = "abc123@some.org" +TEST_SHARE_ID = 'abcdef1234' # NOTE: this is a sample valid but unused ssh public key as it must be parsable TEST_USER_PUB_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuJrICshi7S2KhV03qvgNVOx5ejmHsswdGbvR34wf+eN23Ghq6OZhwGye2S+J6LPVFI3p4SCqxX4URnUM8BRAsiuvbf/+GQfE2pAO0C+/g4V3hhYbYzIyrtPsP1Hl8GioxvZD5nDoLEA4TWokDC4D7SRfv+NEkFLplyVBwHtpUunXBS/zXYdQ4lgk7u8HBBCMqUGbZHfCc+6ibFVn/5WS6vVokL+fSWtxg9tUVWqsS/xtDGPH1wbJUf1Dm3D58KmdX8ca73tBoScwH8qUQwEcyM1JtWtbv1BAZFb+Qk6SEe4GPRsn3I4AAgC7xtU3HKQsiqe8Fzpick/uI5PU+vguitcV/9+AASnGVZJ9M+a63UvlFFloEYcI1LwdZ03JYPQXfXCzJSYiA+pTX4/cf10G4rlxsque4m4OcuCwLKvpTWA/Lla+UJqYhdQW+m7mSizPRoDPgh8mOta1PQob2sGSw8rhqLfApptAPZ0mkN0QY3Dv3i3ItgpYGcPNVXVdjmhU=' @@ -99,7 +100,7 @@ def test_get_creds_changes_detects_new_files(self): past_timestamp = time.time() - 3600 dummy_user = Login( configuration=self.configuration, - username='dummy', + username=TEST_USER_SHORT_ID, home='dummy_home', password=TEST_USER_PW_HASH, digest=None, @@ -133,9 +134,6 @@ def test_get_creds_changes_no_changes(self): # Set the file modification times to a time in the past current_time = time.time() past_timestamp = time.time() - 3600 - # os.utime(self.auth_keys_path, (past_timestamp, past_timestamp)) - # os.utime(self.auth_passwords_path, (past_timestamp, past_timestamp)) - # os.utime(self.auth_digests_path, (past_timestamp, past_timestamp)) # Create a dummy user with last_update matching the file mtime dummy_user = Login( @@ -168,21 +166,20 @@ def test_get_share_changes_detects_updates(self): daemon_conf = self.configuration.daemon_conf daemon_conf['allow_publickey'] = True - share_id = 'Abcdef1234' user_shared_dir = os.path.join(self.configuration.user_home, 'TestUser', 'shared', 'data') ensure_dirs_exist(user_shared_dir) user_shared_keys = os.path.join(user_shared_dir, '.ssh', 'authorized_keys') share_link_path = os.path.join(self.configuration.sharelink_home, - share_id) + TEST_SHARE_ID) os.symlink(user_shared_dir, share_link_path) # Create a dummy share with a last_update in the past past_timestamp = time.time() - 3600 dummy_share = Login( configuration=self.configuration, - username=share_id, + username=TEST_SHARE_ID, home='share_home', password=TEST_USER_PW_HASH, digest=None, @@ -197,7 +194,7 @@ def test_get_share_changes_detects_updates(self): changed_paths = get_share_changes( daemon_conf, - share_id, + TEST_SHARE_ID, share_link_path, self.auth_keys_path ) From a8a10886f7580dabe82636e342f50ae6d3798c11 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 10 Jun 2026 14:47:26 +0200 Subject: [PATCH 22/61] Split tests into individual classes to make it easier to target required setup. --- tests/test_mig_shared_griddaemons.py | 223 +++++++++++++++++++++++++-- 1 file changed, 214 insertions(+), 9 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index eb5ede706..1ee53d40d 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -3,7 +3,7 @@ # # --- BEGIN_HEADER --- # -# test_mig_shared_griddaemons - unit tests for the griddaemons helpers +# test_mig_shared_griddaemons - unit tests for the griddaemons helper functions # Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH # # This file is part of MiG. @@ -20,7 +20,7 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # --- END_HEADER --- # @@ -38,14 +38,13 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( - Login, get_creds_changes, get_share_changes + Login, get_creds_changes, get_share_changes, get_job_changes ) # Imports required for the unit tests themselves from tests.support import ( ensure_dirs_exist, - MigTestCase, temppath -) + MigTestCase, temppath) TEST_USER_SHORT_ID = "abc123@some.org" TEST_SHARE_ID = 'abcdef1234' @@ -56,8 +55,8 @@ TEST_USER_PW_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM1234uxi" -class MigSharedGriddaemonsLogin(MigTestCase): - """Unit tests for griddaemons login helper functions""" +class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): + """Unit tests for griddaemons login get_creds_changes function""" def _provide_configuration(self): """Return a test configuration instance""" @@ -70,8 +69,6 @@ def before_each(self): self.configuration.daemon_conf = {} self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['shares'] = [] - self.configuration.daemon_conf['jobs'] = [] # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True @@ -161,6 +158,39 @@ def test_get_creds_changes_no_changes(self): self.assertEqual(len(changed_paths), 0) + +class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): + """Unit tests for griddaemons login get_share_changes helper function""" + + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test""" + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['shares'] = [] + + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False + + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + # self.auth_digests_path = temppath('authhorized_digests', self) + self.auth_digests_path = None + + # Create sample credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + # with open(self.auth_digests_path, 'w') as creds_fd: + # creds_fd.write(TEST_USER_DIGEST) + def test_get_share_changes_detects_updates(self): """Verify that share link and key file changes are detected""" daemon_conf = self.configuration.daemon_conf @@ -192,6 +222,28 @@ def test_get_share_changes_detects_updates(self): dummy_share.last_update = past_timestamp daemon_conf['shares'].append(dummy_share) + changed_paths = get_share_changes( + daemon_conf, + TEST_SHARE_ID, + share_link_path, + user_shared_keys + ) + + self.assertIn(share_link_path, changed_paths) + self.assertIn(user_shared_keys, changed_paths) + + def test_get_share_changes_new_share(self): + """Verify that a new share link is detected as a change""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True + + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') + ensure_dirs_exist(user_shared_dir) + share_link_path = os.path.join(self.configuration.sharelink_home, + TEST_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + changed_paths = get_share_changes( daemon_conf, TEST_SHARE_ID, @@ -200,6 +252,159 @@ def test_get_share_changes_detects_updates(self): ) self.assertIn(share_link_path, changed_paths) + self.assertIn(self.auth_keys_path, changed_paths) + + def test_get_share_changes_no_changes(self): + """Verify that unchanged share files return an empty list""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True + + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') + ensure_dirs_exist(user_shared_dir) + user_shared_keys = os.path.join(user_shared_dir, '.ssh', + 'authorized_keys') + ensure_dirs_exist(user_shared_keys) + share_link_path = os.path.join(self.configuration.sharelink_home, + TEST_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + # Create a dummy share with last_update matching file mtime + current_time = time.time() + dummy_share = Login( + configuration=self.configuration, + username=TEST_SHARE_ID, + home='share_home', + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_share.last_update = current_time + daemon_conf['shares'].append(dummy_share) + + changed_paths = get_share_changes( + daemon_conf, + TEST_SHARE_ID, + share_link_path, + user_shared_keys + ) + + self.assertEqual(len(changed_paths), 0) + + +class MigSharedGriddaemonsLogin__get_job_changes(MigTestCase): + """Unit tests for griddaemons login get_job_changes function""" + + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test""" + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['jobs'] = [] + + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False + + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + # self.auth_digests_path = temppath('authhorized_digests', self) + self.auth_digests_path = None + + # Create sample credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + # with open(self.auth_digests_path, 'w') as creds_fd: + # creds_fd.write(TEST_USER_DIGEST) + + def test_get_job_changes_new_job(self): + """Verify that a new job mrsl file is detected as a change""" + daemon_conf = self.configuration.daemon_conf + + mrsl_path = temppath('test_job.mRSL', self) + with open(mrsl_path, 'w') as mrsl_fd: + mrsl_fd.write('test content') + + changed_paths = get_job_changes( + daemon_conf, + 'test_session_id', + mrsl_path + ) + + self.assertIn(mrsl_path, changed_paths) + + def test_get_job_changes_no_changes(self): + """Verify that unchanged job mrsl file returns an empty list""" + daemon_conf = self.configuration.daemon_conf + + mrsl_path = temppath('test_job.mRSL', self) + with open(mrsl_path, 'w') as mrsl_fd: + mrsl_fd.write('test content') + + # Create a dummy job with last_update matching file mtime + current_time = time.time() + dummy_job = Login( + configuration=self.configuration, + username='test_session_id', + home='job_home', + password=None, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_job.last_update = current_time + daemon_conf['jobs'].append(dummy_job) + + changed_paths = get_job_changes( + daemon_conf, + 'test_session_id', + mrsl_path + ) + + self.assertEqual(len(changed_paths), 0) + + def test_get_job_changes_missing_file(self): + """Verify that missing mrsl file is detected for existing job""" + daemon_conf = self.configuration.daemon_conf + + # Create a dummy job + dummy_job = Login( + configuration=self.configuration, + username='test_session_id', + home='job_home', + password=None, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + daemon_conf['jobs'].append(dummy_job) + + mrsl_path = temppath('missing_job.mRSL', self) + + changed_paths = get_job_changes( + daemon_conf, + 'test_session_id', + mrsl_path + ) + + self.assertIn(mrsl_path, changed_paths) if __name__ == '__main__': From c00cfcd3bd4bc5c353653e87bae36fdab5b74451 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 10 Jun 2026 15:30:30 +0200 Subject: [PATCH 23/61] Add tests for refresh_share_creds. --- tests/test_mig_shared_griddaemons.py | 157 +++++++++++++++++++++++++-- 1 file changed, 145 insertions(+), 12 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 1ee53d40d..c048181c5 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -20,7 +20,7 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # --- END_HEADER --- # @@ -32,13 +32,16 @@ import unittest # Imports required for the unit test wrapping +from mig.shared.defaults import X509_USER_ID_FORMAT, UUID_USER_ID_FORMAT, \ + READ_WRITE_ACCESS, READ_ONLY_ACCESS, WRITE_ONLY_ACCESS from mig.shared.useradm import ( - _ensure_dirs_needed_for_userdb, + _ensure_dirs_needed_for_userdb # , generate_password_hash ) # Imports of the code under test from mig.shared.griddaemons.login import ( - Login, get_creds_changes, get_share_changes, get_job_changes + Login, get_creds_changes, get_share_changes, get_job_changes, + refresh_share_creds ) # Imports required for the unit tests themselves @@ -47,7 +50,9 @@ MigTestCase, temppath) TEST_USER_SHORT_ID = "abc123@some.org" -TEST_SHARE_ID = 'abcdef1234' +TEST_RO_SHARE_ID = 'abcdef1234' +TEST_RW_SHARE_ID = 'klmnop4567' +TEST_WO_SHARE_ID = 'uvwxyz7890' # NOTE: this is a sample valid but unused ssh public key as it must be parsable TEST_USER_PUB_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuJrICshi7S2KhV03qvgNVOx5ejmHsswdGbvR34wf+eN23Ghq6OZhwGye2S+J6LPVFI3p4SCqxX4URnUM8BRAsiuvbf/+GQfE2pAO0C+/g4V3hhYbYzIyrtPsP1Hl8GioxvZD5nDoLEA4TWokDC4D7SRfv+NEkFLplyVBwHtpUunXBS/zXYdQ4lgk7u8HBBCMqUGbZHfCc+6ibFVn/5WS6vVokL+fSWtxg9tUVWqsS/xtDGPH1wbJUf1Dm3D58KmdX8ca73tBoScwH8qUQwEcyM1JtWtbv1BAZFb+Qk6SEe4GPRsn3I4AAgC7xtU3HKQsiqe8Fzpick/uI5PU+vguitcV/9+AASnGVZJ9M+a63UvlFFloEYcI1LwdZ03JYPQXfXCzJSYiA+pTX4/cf10G4rlxsque4m4OcuCwLKvpTWA/Lla+UJqYhdQW+m7mSizPRoDPgh8mOta1PQob2sGSw8rhqLfApptAPZ0mkN0QY3Dv3i3ItgpYGcPNVXVdjmhU=' @@ -68,6 +73,7 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 self.configuration.daemon_conf['users'] = [] # TODO: enable and test unsafe digest auth, too? @@ -172,6 +178,7 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 self.configuration.daemon_conf['shares'] = [] # TODO: enable and test unsafe digest auth, too? @@ -202,14 +209,14 @@ def test_get_share_changes_detects_updates(self): user_shared_keys = os.path.join(user_shared_dir, '.ssh', 'authorized_keys') share_link_path = os.path.join(self.configuration.sharelink_home, - TEST_SHARE_ID) + TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) # Create a dummy share with a last_update in the past past_timestamp = time.time() - 3600 dummy_share = Login( configuration=self.configuration, - username=TEST_SHARE_ID, + username=TEST_RO_SHARE_ID, home='share_home', password=TEST_USER_PW_HASH, digest=None, @@ -224,7 +231,7 @@ def test_get_share_changes_detects_updates(self): changed_paths = get_share_changes( daemon_conf, - TEST_SHARE_ID, + TEST_RO_SHARE_ID, share_link_path, user_shared_keys ) @@ -241,12 +248,12 @@ def test_get_share_changes_new_share(self): 'TestUser', 'shared', 'data') ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.configuration.sharelink_home, - TEST_SHARE_ID) + TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) changed_paths = get_share_changes( daemon_conf, - TEST_SHARE_ID, + TEST_RO_SHARE_ID, share_link_path, self.auth_keys_path ) @@ -266,14 +273,14 @@ def test_get_share_changes_no_changes(self): 'authorized_keys') ensure_dirs_exist(user_shared_keys) share_link_path = os.path.join(self.configuration.sharelink_home, - TEST_SHARE_ID) + TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) # Create a dummy share with last_update matching file mtime current_time = time.time() dummy_share = Login( configuration=self.configuration, - username=TEST_SHARE_ID, + username=TEST_RO_SHARE_ID, home='share_home', password=TEST_USER_PW_HASH, digest=None, @@ -288,7 +295,7 @@ def test_get_share_changes_no_changes(self): changed_paths = get_share_changes( daemon_conf, - TEST_SHARE_ID, + TEST_RO_SHARE_ID, share_link_path, user_shared_keys ) @@ -309,6 +316,7 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 self.configuration.daemon_conf['jobs'] = [] # TODO: enable and test unsafe digest auth, too? @@ -407,5 +415,130 @@ def test_get_job_changes_missing_file(self): self.assertIn(mrsl_path, changed_paths) +class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): + """Unit tests for the griddaemons login refresh_share_creds helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # The base class already creates the required directory layout. + # Ensure the share‑link home exists – it is used by refresh_share_creds. + self.ro_share_home = os.path.join(self.configuration.sharelink_home, + 'read-only') + self.rw_share_home = os.path.join(self.configuration.sharelink_home, + 'read-write') + self.wo_share_home = os.path.join(self.configuration.sharelink_home, + 'write-only') + ensure_dirs_exist(self.ro_share_home) + ensure_dirs_exist(self.rw_share_home) + ensure_dirs_exist(self.rw_share_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + + # Paths that the function will look at + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + self.auth_digests_path = None + + # Create dummy credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + + def test_refresh_share_creds_adds_new_share(self): + """A new share link should be added to daemon_conf['shares'].""" + daemon_conf = self.configuration.daemon_conf + + # Build a share link that points to a temporary user directory + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + # Call the function under test + # NOTE: only sftp access is supported for now + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) + ) + + # The share should now be present in the changed list + self.assertIn(TEST_RW_SHARE_ID, changed_shares) + + # Verify that a Login object was added to shares + share_login = [ + obj for obj in updated_conf['shares'] + if obj.username == TEST_RW_SHARE_ID + ] + self.assertEqual(len(share_login), 1) + + # The added object should contain the expected home directory + self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) + self.assertEqual(share_login[0].home, rel_share_home) + # TODO: check password hash, too? + # share_pw_hash = generate_password_hash(self.configuration, + # TEST_RW_SHARE_ID) + # self.assertEqual(share_login[0].password, share_pw_hash) + + def test_refresh_share_creds_no_changes(self): + """When the share link and its key file have not changed, the function + should return an empty changed_shares list.""" + daemon_conf = self.configuration.daemon_conf + + # Create a share link that already exists + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + # Populate shares with a dummy entry whose last_update matches + # the current file mtime – this simulates “no changes”. + current_time = time.time() + dummy_share = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=rel_share_home, + password=TEST_RW_SHARE_ID, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None) + dummy_share.last_update = current_time + daemon_conf['shares'].append(dummy_share) + + # Run the function + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) + ) + + # No changes should be reported + self.assertEqual(len(changed_shares), 0) + + # The dummy entry should still be the only one present + self.assertEqual(len(updated_conf['shares']), 1) + + if __name__ == '__main__': unittest.main() From 3da033d45fd23e6f0e7b4f5684d39afdf26fcef6 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 10 Jun 2026 16:07:24 +0200 Subject: [PATCH 24/61] Simplify setup with shared helper vars and extend test coverage for the refresh_share_creds function. --- tests/test_mig_shared_griddaemons.py | 161 +++++++++++++++++++++++++-- 1 file changed, 153 insertions(+), 8 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index c048181c5..08227cfa3 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -175,7 +175,15 @@ def _provide_configuration(self): def before_each(self): """Set up test configuration and reset state before each test""" _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) + self.ro_share_home = os.path.join(self.configuration.sharelink_home, + 'read-only') + self.rw_share_home = os.path.join(self.configuration.sharelink_home, + 'read-write') + self.wo_share_home = os.path.join(self.configuration.sharelink_home, + 'write-only') + ensure_dirs_exist(self.ro_share_home) + ensure_dirs_exist(self.rw_share_home) + ensure_dirs_exist(self.wo_share_home) self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 @@ -208,8 +216,7 @@ def test_get_share_changes_detects_updates(self): ensure_dirs_exist(user_shared_dir) user_shared_keys = os.path.join(user_shared_dir, '.ssh', 'authorized_keys') - share_link_path = os.path.join(self.configuration.sharelink_home, - TEST_RO_SHARE_ID) + share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) # Create a dummy share with a last_update in the past @@ -247,8 +254,7 @@ def test_get_share_changes_new_share(self): user_shared_dir = os.path.join(self.configuration.user_home, 'TestUser', 'shared', 'data') ensure_dirs_exist(user_shared_dir) - share_link_path = os.path.join(self.configuration.sharelink_home, - TEST_RO_SHARE_ID) + share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) changed_paths = get_share_changes( @@ -272,8 +278,7 @@ def test_get_share_changes_no_changes(self): user_shared_keys = os.path.join(user_shared_dir, '.ssh', 'authorized_keys') ensure_dirs_exist(user_shared_keys) - share_link_path = os.path.join(self.configuration.sharelink_home, - TEST_RO_SHARE_ID) + share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) # Create a dummy share with last_update matching file mtime @@ -434,7 +439,7 @@ def before_each(self): 'write-only') ensure_dirs_exist(self.ro_share_home) ensure_dirs_exist(self.rw_share_home) - ensure_dirs_exist(self.rw_share_home) + ensure_dirs_exist(self.wo_share_home) self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 @@ -539,6 +544,146 @@ def test_refresh_share_creds_no_changes(self): # The dummy entry should still be the only one present self.assertEqual(len(updated_conf['shares']), 1) + def test_refresh_share_creds_adds_readonly_share(self): + """Test that a read‑only share is added correctly""" + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.configuration.sharelink_home, + 'read-only', TEST_RO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RO_SHARE_ID, + share_modes=(READ_ONLY_ACCESS, ) + ) + + self.assertIn(TEST_RO_SHARE_ID, changed_shares) + share_login = [obj for obj in updated_conf['shares'] + if obj.username == TEST_RO_SHARE_ID] + self.assertEqual(len(share_login), 1) + self.assertEqual(share_login[0].home, rel_share_home) + + def test_refresh_share_creds_adds_writeonly_share(self): + """Test that a write‑only share is added correctly""" + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.wo_share_home, TEST_WO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_WO_SHARE_ID, + share_modes=(WRITE_ONLY_ACCESS, ) + ) + + self.assertIn(TEST_WO_SHARE_ID, changed_shares) + share_login = [obj for obj in updated_conf['shares'] + if obj.username == TEST_WO_SHARE_ID] + self.assertEqual(len(share_login), 1) + self.assertEqual(share_login[0].home, rel_share_home) + + def test_refresh_share_creds_no_change_on_unchanged_link(self): + """Test that an unchanged share link does not trigger a change""" + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + # Populate shares with a dummy entry whose last_update matches + # the current file mtime – this simulates “no changes”. + current_time = time.time() + dummy_share = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=rel_share_home, + password=TEST_RW_SHARE_ID, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None) + dummy_share.last_update = current_time + self.configuration.daemon_conf['shares'].append(dummy_share) + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) + ) + + self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(updated_conf['shares']), 1) + + def test_refresh_share_creds_detects_missing_link(self): + """Test that a missing share link is reported as a change""" + # No symlink created - share link is missing + missing_share_id = 'missing123' + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=missing_share_id, + share_modes=(READ_WRITE_ACCESS, ) + ) + + # The function should still return an empty list because the link + # does not exist; no share is added. + self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(updated_conf['shares']), 0) + + def test_refresh_share_creds_ignores_dead_link(self): + """Test that a dead share link is ignored""" + # Create a symlink that points nowhere + invalid_target = os.path.join(self.configuration.user_home, 'deadbeef') + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(invalid_target, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) + ) + + # No share should be added because the link is invalid + self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(updated_conf['shares']), 0) + + def test_refresh_share_creds_ignores_invalid_link(self): + """Test that an invalid (out of bounds) share link is ignored""" + # Create a symlink that points outside a user_home directory + invalid_target = self.configuration.certs_path + ensure_dirs_exist(invalid_target) + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(invalid_target, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) + ) + + # No share should be added because the link is invalid but change + # is still reported as modified. + self.assertEqual(len(changed_shares), 1) + self.assertEqual(changed_shares[0], TEST_RW_SHARE_ID) + self.assertEqual(len(updated_conf['shares']), 0) + if __name__ == '__main__': unittest.main() From bcc0472c3eda05570622754da40f9e1e9788a0e9 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 10 Jun 2026 22:39:44 +0200 Subject: [PATCH 25/61] Add test coverage of refresh_user_creds in griddaemons suite and extend refresh_share_creds tests to also check shares with both password and key. --- tests/test_mig_shared_griddaemons.py | 320 ++++++++++++++++++++++++--- 1 file changed, 294 insertions(+), 26 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 08227cfa3..31e424f41 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -32,8 +32,10 @@ import unittest # Imports required for the unit test wrapping -from mig.shared.defaults import X509_USER_ID_FORMAT, UUID_USER_ID_FORMAT, \ +from mig.shared.defaults import ( + # X509_USER_ID_FORMAT, UUID_USER_ID_FORMAT, READ_WRITE_ACCESS, READ_ONLY_ACCESS, WRITE_ONLY_ACCESS +) from mig.shared.useradm import ( _ensure_dirs_needed_for_userdb # , generate_password_hash ) @@ -41,15 +43,20 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( Login, get_creds_changes, get_share_changes, get_job_changes, - refresh_share_creds + refresh_share_creds, refresh_user_creds ) # Imports required for the unit tests themselves from tests.support import ( - ensure_dirs_exist, - MigTestCase, temppath) + ensure_dirs_exist, MigTestCase, temppath, UserAssertMixin +) +from tests.support.usersupp import ( + TEST_USER_DN +) +TEST_USER_UUID = "UniqueUserIdForTestUser" TEST_USER_SHORT_ID = "abc123@some.org" +TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] TEST_RO_SHARE_ID = 'abcdef1234' TEST_RW_SHARE_ID = 'klmnop4567' TEST_WO_SHARE_ID = 'uvwxyz7890' @@ -60,6 +67,33 @@ TEST_USER_PW_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM1234uxi" +def _prepare_auth_files(home_path, auth_protos=None): + """Helper to create helper auth files for on eor more auth_protos. + If None is passed ssh, ftps and davs auth files will be made. + """ + auth_files = [] + if auth_protos is None: + auth_protos = ['davs', 'ftps', 'ssh'] + + # Create requested auth dirs with files + for auth in auth_protos: + # Create a .PROTO directory with authorized_X file(s) + dot_proto_dir = os.path.join(home_path, '.%s' % auth) + ensure_dirs_exist(dot_proto_dir) + if auth == 'ssh': + authkeys_path = os.path.join(dot_proto_dir, 'authorized_keys') + with open(authkeys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + auth_files.append(authkeys_path) + + authpasswords_path = os.path.join(dot_proto_dir, + 'authorized_passwords') + with open(authpasswords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + auth_files.append(authpasswords_path) + return auth_files + + class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): """Unit tests for griddaemons login get_creds_changes function""" @@ -75,36 +109,27 @@ def before_each(self): self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 self.configuration.daemon_conf['users'] = [] - + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True self.configuration.daemon_conf['allow_digest'] = False - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - # self.auth_digests_path = temppath('authhorized_digests', self) + self.test_user_home = self._provision_test_user(self, TEST_USER_DN) + auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + self.auth_keys_path, self.auth_passwords_path = auth_files self.auth_digests_path = None - # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - # with open(self.auth_digests_path, 'w') as creds_fd: - # creds_fd.write(TEST_USER_DIGEST) - def test_get_creds_changes_detects_new_files(self): """Verify that new credential files are detected as changes""" daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True - daemon_conf['allow_password'] = True # Create a dummy user with a last_update in the past past_timestamp = time.time() - 3600 dummy_user = Login( configuration=self.configuration, username=TEST_USER_SHORT_ID, - home='dummy_home', + home=self.test_user_home, password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, @@ -131,18 +156,15 @@ def test_get_creds_changes_detects_new_files(self): def test_get_creds_changes_no_changes(self): """Verify that unchanged credential files return an empty list""" daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True - daemon_conf['allow_password'] = True - # Set the file modification times to a time in the past + # Set the file modification times to now current_time = time.time() - past_timestamp = time.time() - 3600 # Create a dummy user with last_update matching the file mtime dummy_user = Login( configuration=self.configuration, username=TEST_USER_SHORT_ID, - home='dummy_home', + home=self.test_user_home, password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, @@ -461,8 +483,6 @@ def before_each(self): def test_refresh_share_creds_adds_new_share(self): """A new share link should be added to daemon_conf['shares'].""" - daemon_conf = self.configuration.daemon_conf - # Build a share link that points to a temporary user directory rel_share_home = os.path.join('TestUser', 'shared', 'data') user_shared_dir = os.path.join(self.configuration.user_home, @@ -499,6 +519,52 @@ def test_refresh_share_creds_adds_new_share(self): # TEST_RW_SHARE_ID) # self.assertEqual(share_login[0].password, share_pw_hash) + def test_refresh_share_creds_adds_new_share_with_key(self): + """A new share link with key should be added twice to daemon_conf['shares'].""" + # Build a share link that points to a temporary user directory + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + _prepare_auth_files(user_shared_dir, ['ssh']) + + # Call the function under test + # NOTE: only sftp access is supported for now + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) + ) + + # The share should now be present twice in the changed list + self.assertIn(TEST_RW_SHARE_ID, changed_shares) + + # Verify that a Login object was added to shares + share_login = [ + obj for obj in updated_conf['shares'] + if obj.username == TEST_RW_SHARE_ID + ] + self.assertEqual(len(share_login), 2) + + # The added objects should contain the expected home directory + self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) + self.assertEqual(share_login[0].home, rel_share_home) + self.assertEqual(share_login[1].username, TEST_RW_SHARE_ID) + self.assertEqual(share_login[1].home, rel_share_home) + # TODO: check password hash, too? + # share_pw_hash = generate_password_hash(self.configuration, + # TEST_RW_SHARE_ID) + # self.assertEqual(share_login[0].password, share_pw_hash) + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = share_login[1].public_key + result = "%s %s" % (login_key.get_name(), login_key.get_base64()) + self.assertEqual(result, TEST_USER_PUB_KEY) + def test_refresh_share_creds_no_changes(self): """When the share link and its key file have not changed, the function should return an empty changed_shares list.""" @@ -685,5 +751,207 @@ def test_refresh_share_creds_ignores_invalid_link(self): self.assertEqual(len(updated_conf['shares']), 0) +class MigSharedGriddaemonsLogin__refresh_user_creds(MigTestCase, UserAssertMixin): + """Unit tests for the griddaemons login refresh_user_creds helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Ensure required directories exist + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + self.configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + self.test_user_home = self._provision_test_user(self, TEST_USER_DN) + self.test_user_dir = os.path.basename(self.test_user_home) + # Make sure alias link is provisioned as well + alias_link_path = os.path.join(self.configuration.user_home, + TEST_USER_EMAIL) + if not os.path.islink(alias_link_path): + os.symlink(self.test_user_home, alias_link_path) + + ALIAS_FIELD = 'email' + self.configuration.user_sftp_alias = ALIAS_FIELD + self.configuration.user_ftps_alias = ALIAS_FIELD + self.configuration.user_davs_alias = ALIAS_FIELD + + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD + + def test_refresh_user_creds_ssh_protocol(self): + """Test refreshing user credentials for SSH protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least one login (the main username) and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + home_found = any(login.home == self.test_user_dir for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_davs_protocol(self): + """Test refreshing user credentials for DAVS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['davs']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='davs', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + self.assertEqual(user_logins[0].home, self.test_user_dir) + + def test_refresh_user_creds_ftps_protocol(self): + """Test refreshing user credentials for FTPS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ftps']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ftps', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + self.assertEqual(user_logins[0].home, self.test_user_dir) + + def test_refresh_user_creds_https_protocol(self): + """Test refreshing user credentials for HTTPS protocol (uses user DB).""" + username = TEST_USER_EMAIL + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='https', + username=username + ) + + # The user alias should be in the changed list + self.assertIn(TEST_USER_EMAIL, changed_users) + + # Verify that Login objects were added to users for the username and its aliases + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least the main username and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + home_found = any(login.home == self.test_user_dir for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_no_changes(self): + """Test that no changes are reported when credentials are unchanged.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Pre-populate the users list with a Login object that has + # last_update set to the current time (simulating no changes) + current_time = time.time() + dummy_user = Login( + configuration=self.configuration, + username=username, + home=self.test_user_home, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None) + dummy_user.last_update = current_time + self.configuration.daemon_conf['users'].append(dummy_user) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # No changes should be reported + self.assertEqual(len(changed_users), 0) + # The user list should still contain only our dummy user + self.assertEqual(len(updated_conf['users']), 1) + self.assertEqual(updated_conf['users'][0].username, username) + + def test_refresh_user_creds_missing_user(self): + """Test that a missing user is skipped.""" + username = 'nosuchuser' + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # No changes should be reported because the home directory is missing + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) + + def test_refresh_user_creds_invalid_protocol(self): + """Test that an invalid protocol returns early without changes.""" + username = TEST_USER_EMAIL + + # Call the function under test with an invalid protocol + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='invalid', + username=username + ) + + # No changes should be reported + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) + + if __name__ == '__main__': unittest.main() From f444a9495c8a97fa1a76cbeb40c105b0b9e9ee2c Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 12 Jun 2026 09:56:16 +0200 Subject: [PATCH 26/61] Add UUID user id version of refresh_user_creds unit tests. --- tests/test_mig_shared_griddaemons.py | 264 ++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 3 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 31e424f41..829e166f7 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -27,17 +27,20 @@ """Unit tests for the griddaemons helper functions""" +import binascii import os import time import unittest # Imports required for the unit test wrapping +from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( - # X509_USER_ID_FORMAT, UUID_USER_ID_FORMAT, - READ_WRITE_ACCESS, READ_ONLY_ACCESS, WRITE_ONLY_ACCESS + X509_USER_ID_FORMAT, UUID_USER_ID_FORMAT, + READ_WRITE_ACCESS, READ_ONLY_ACCESS, WRITE_ONLY_ACCESS, + keyword_auto ) from mig.shared.useradm import ( - _ensure_dirs_needed_for_userdb # , generate_password_hash + _ensure_dirs_needed_for_userdb, create_user # , generate_password_hash ) # Imports of the code under test @@ -67,6 +70,34 @@ TEST_USER_PW_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM1234uxi" +def _provision_uuid_test_user(configuration, client_id, client_overrides=None): + """Helper to provision test users when UUID format is used""" + # TODO: merge something like this version into standard _provision_test_user? + # IMPORTANT: we need to use explicit create_user here for UUID format! + user_dict = distinguished_name_to_user(client_id) + # NOTE: generate unique and short id based on id to avoid test collisions + user_dict["unique_id"] = binascii.hexlify( + client_id.encode('utf8')).decode('ascii') + user_dict["short_id"] = binascii.hexlify( + user_dict["email"].encode('utf8')).decode('ascii') + user_dict["comment"] = "This is the user account comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = "" + if client_overrides is not None: + user_dict.update(client_overrides) + + create_user( + user_dict, + configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + return user_dict + + def _prepare_auth_files(home_path, auth_protos=None): """Helper to create helper auth files for on eor more auth_protos. If None is passed ssh, ftps and davs auth files will be made. @@ -760,6 +791,8 @@ def _provide_configuration(self): def before_each(self): """Set up test configuration and reset state before each test.""" + # Force X509 user id format + self.configuration.site_user_id_format = X509_USER_ID_FORMAT # Ensure required directories exist _ensure_dirs_needed_for_userdb(self.configuration) self.expected_user_db_home = os.path.normpath( @@ -952,6 +985,231 @@ def test_refresh_user_creds_invalid_protocol(self): self.assertEqual(len(changed_users), 0) self.assertEqual(len(updated_conf['users']), 0) +# TODO: merge X509 and UUID versions? + + +class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id(MigTestCase, UserAssertMixin): + """Unit tests for the griddaemons login refresh_user_creds helper with UUID users.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Force UUID user id format + self.configuration.site_user_id_format = UUID_USER_ID_FORMAT + # Ensure required directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + self.configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + # NOTE: we need to set a password_hash for https test to work with + overrides = {} + overrides["password_hash"] = TEST_USER_PW_HASH + + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN, + overrides) + self.test_user_id = user_id = user_dict['unique_id'] + client_dir = client_id_dir(TEST_USER_DN) + self.test_user_home = os.path.join(self.configuration.user_home, + user_id) + self.test_user_dir = os.path.basename(self.test_user_home) + self.test_user_home_x509 = os.path.join(self.configuration.user_home, + client_dir) + self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) + # Make sure X509 and alias links are provisioned as well + if not os.path.islink(self.test_user_home_x509): + os.symlink(self.test_user_home, self.test_user_home_x509) + alias_link_path = os.path.join(self.configuration.user_home, + TEST_USER_EMAIL) + if not os.path.islink(alias_link_path): + os.symlink(self.test_user_home_x509, alias_link_path) + + ALIAS_FIELD = 'email' + self.configuration.user_sftp_alias = ALIAS_FIELD + self.configuration.user_ftps_alias = ALIAS_FIELD + self.configuration.user_davs_alias = ALIAS_FIELD + + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD + + def test_refresh_user_creds_ssh_protocol(self): + """Test refreshing user credentials for SSH protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least one login (the main username) and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + # TODO: is this X509 dir what we want here or the UUID one? + home_found = any(login.home == self.test_user_dir_x509 for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_davs_protocol(self): + """Test refreshing user credentials for DAVS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['davs']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='davs', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + # TODO: is this X509 dir what we want here or the UUID one? + self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + + def test_refresh_user_creds_ftps_protocol(self): + """Test refreshing user credentials for FTPS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ftps']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ftps', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + # TODO: is this X509 dir what we want here or the UUID one? + self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + + def test_refresh_user_creds_https_protocol(self): + """Test refreshing user credentials for HTTPS protocol (uses user DB).""" + username = TEST_USER_EMAIL + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='https', + username=username + ) + + # The user alias should be in the changed list + self.assertIn(TEST_USER_EMAIL, changed_users) + + # Verify that Login objects were added to users for the username and its aliases + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least the main username and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + # TODO: is this X509 dir what we want here or the UUID one? + home_found = any(login.home == self.test_user_dir_x509 for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_no_changes(self): + """Test that no changes are reported when credentials are unchanged.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Pre-populate the users list with a Login object that has + # last_update set to the current time (simulating no changes) + current_time = time.time() + dummy_user = Login( + configuration=self.configuration, + username=username, + home=self.test_user_home, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None) + dummy_user.last_update = current_time + self.configuration.daemon_conf['users'].append(dummy_user) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # No changes should be reported + self.assertEqual(len(changed_users), 0) + # The user list should still contain only our dummy user + self.assertEqual(len(updated_conf['users']), 1) + self.assertEqual(updated_conf['users'][0].username, username) + + def test_refresh_user_creds_missing_user(self): + """Test that a missing user is skipped.""" + username = 'nosuchuser' + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # No changes should be reported because the home directory is missing + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) + + def test_refresh_user_creds_invalid_protocol(self): + """Test that an invalid protocol returns early without changes.""" + username = TEST_USER_EMAIL + + # Call the function under test with an invalid protocol + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='invalid', + username=username + ) + + # No changes should be reported + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) + if __name__ == '__main__': unittest.main() From 58e220a4024867b6aa1f832c69ab5c24a8f4f9da Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 12 Jun 2026 11:14:36 +0200 Subject: [PATCH 27/61] Add UUID user id version of get_creds_changes unit tests. --- tests/test_mig_shared_griddaemons.py | 114 +++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 829e166f7..4f92f1972 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -134,6 +134,9 @@ def _provide_configuration(self): def before_each(self): """Set up test configuration and reset state before each test""" + # Force X509 user id format + self.configuration.site_user_id_format = X509_USER_ID_FORMAT + _ensure_dirs_needed_for_userdb(self.configuration) ensure_dirs_exist(self.configuration.sharelink_home) @@ -218,6 +221,117 @@ def test_get_creds_changes_no_changes(self): self.assertEqual(len(changed_paths), 0) +class MigSharedGriddaemonsLogin__get_creds_changes_uuid_user_id(MigTestCase): + """Unit tests for griddaemons login get_creds_changes function with UUID users""" + + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test""" + # Force UUID user id format + self.configuration.site_user_id_format = UUID_USER_ID_FORMAT + # Ensure required directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False + + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) + self.test_user_id = user_id = user_dict['unique_id'] + client_dir = client_id_dir(TEST_USER_DN) + self.test_user_home = os.path.join(self.configuration.user_home, + user_id) + self.test_user_dir = os.path.basename(self.test_user_home) + self.test_user_home_x509 = os.path.join(self.configuration.user_home, + client_dir) + self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) + # Make sure X509 link are provisioned as well + if not os.path.islink(self.test_user_home_x509): + os.symlink(self.test_user_home, self.test_user_home_x509) + + auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + self.auth_keys_path, self.auth_passwords_path = auth_files + self.auth_digests_path = None + + def test_get_creds_changes_detects_new_files(self): + """Verify that new credential files are detected as changes""" + daemon_conf = self.configuration.daemon_conf + + # Create a dummy user with a last_update in the past + past_timestamp = time.time() - 3600 + # NOTE: we use x509 alias over uuid home as in real runs + dummy_user = Login( + configuration=self.configuration, + username=TEST_USER_SHORT_ID, + home=self.test_user_home_x509, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_user.last_update = past_timestamp + daemon_conf['users'].append(dummy_user) + + changed_paths = get_creds_changes( + daemon_conf, + 'user', + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path + ) + + self.assertIn(self.auth_keys_path, changed_paths) + self.assertIn(self.auth_passwords_path, changed_paths) + # self.assertIn(self.auth_digests_path, changed_paths) + + def test_get_creds_changes_no_changes(self): + """Verify that unchanged credential files return an empty list""" + daemon_conf = self.configuration.daemon_conf + + # Set the file modification times to now + current_time = time.time() + + # Create a dummy user with last_update matching the file mtime + # NOTE: we use x509 alias over uuid home as in real runs + dummy_user = Login( + configuration=self.configuration, + username=TEST_USER_SHORT_ID, + home=self.test_user_home_x509, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_user.last_update = current_time + daemon_conf['users'].append(dummy_user) + + changed_paths = get_creds_changes( + daemon_conf, + TEST_USER_SHORT_ID, + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path + ) + + self.assertEqual(len(changed_paths), 0) + + class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): """Unit tests for griddaemons login get_share_changes helper function""" From 8542b0ac5c3f82e72d744991a72ce8e933101e4f Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 12 Jun 2026 11:48:53 +0200 Subject: [PATCH 28/61] Add unit tests for login_map_lookup helper. --- tests/test_mig_shared_griddaemons.py | 101 ++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 4f92f1972..5f730e85d 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -29,6 +29,7 @@ import binascii import os +import threading import time import unittest @@ -46,7 +47,7 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( Login, get_creds_changes, get_share_changes, get_job_changes, - refresh_share_creds, refresh_user_creds + login_map_lookup, refresh_share_creds, refresh_user_creds ) # Imports required for the unit tests themselves @@ -1325,5 +1326,103 @@ def test_refresh_user_creds_invalid_protocol(self): self.assertEqual(len(updated_conf['users']), 0) +class MigSharedGriddaemonsLogin__login_map_lookup(MigTestCase): + """Unit tests for the login_map_lookup function from griddaemons/login.py""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration with login_map and creds_lock.""" + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['login_map'] = {} + self.configuration.daemon_conf['creds_lock'] = threading.Lock() + + def test_user_exists_with_multiple_credentials(self): + """Verify login_map_lookup returns all credentials for a user.""" + # Create two Login objects for 'user1' + cred1 = Login( + configuration=self.configuration.daemon_conf, + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + cred2 = Login( + configuration=self.configuration, + username='user1', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + # Populate login_map + daemon_conf = self.configuration.daemon_conf + daemon_conf['login_map']['user1'] = [cred1, cred2] + # Call the function + result = login_map_lookup(daemon_conf, 'user1') + # Assert + self.assertEqual(result, [cred1, cred2]) + + def test_user_exists_with_single_credential(self): + """Verify login_map_lookup returns one credential for a user.""" + cred = Login( + configuration=self.configuration, + username='user2', + home='home3', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + daemon_conf = self.configuration.daemon_conf + daemon_conf['login_map']['user2'] = [cred] + result = login_map_lookup(daemon_conf, 'user2') + self.assertEqual(result, [cred]) + + def test_user_does_not_exist(self): + """Verify login_map_lookup returns empty list for non-existent user.""" + daemon_conf = self.configuration.daemon_conf + result = login_map_lookup(daemon_conf, 'nosuchuser') + self.assertEqual(result, []) + + def test_lock_handling(self): + """Basic check that creds_lock is acquired and released.""" + # Simulate concurrent access (not fully testable in unit tests) + # This is a placeholder for thread-safety verification + daemon_conf = self.configuration.daemon_conf + cred = Login( + configuration=self.configuration, + username='user3', + home='home4', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + daemon_conf['login_map']['user3'] = [cred] + # Call the function (lock should be acquired/released internally) + result = login_map_lookup(daemon_conf, 'user3') + self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) + + if __name__ == '__main__': unittest.main() From fdb61cc20f8f8f18bf15460c9ed001fad1afe995 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 12 Jun 2026 11:58:16 +0200 Subject: [PATCH 29/61] Add update_login_map unit tests. --- tests/test_mig_shared_griddaemons.py | 283 ++++++++++++++++++++++++++- 1 file changed, 282 insertions(+), 1 deletion(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 5f730e85d..b9d299a47 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -47,7 +47,7 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( Login, get_creds_changes, get_share_changes, get_job_changes, - login_map_lookup, refresh_share_creds, refresh_user_creds + login_map_lookup, refresh_share_creds, refresh_user_creds, update_login_map ) # Imports required for the unit tests themselves @@ -1424,5 +1424,286 @@ def test_lock_handling(self): self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) +class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): + """Unit tests for the update_login_map function from griddaemons/login.py""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration with login_map and creds_lock.""" + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['login_map'] = {} + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['jobs'] = [] + self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['jupyter_mounts'] = [] + self.configuration.daemon_conf['creds_lock'] = threading.Lock() + + def test_update_login_map_users(self): + """Verify login_map is updated correctly for changed users.""" + # Create Login objects + user1 = Login( + configuration=self.configuration, + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + user2 = Login( + configuration=self.configuration, + username='user2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + # Populate users list + self.configuration.daemon_conf['users'] = [user1, user2] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=['user1', 'user2'] + ) + + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('user1', login_map) + self.assertIn('user2', login_map) + self.assertEqual(login_map['user1'], [user1]) + self.assertEqual(login_map['user2'], [user2]) + + def test_update_login_map_jobs(self): + """Verify login_map is updated correctly for changed jobs.""" + # Create Login objects + job1 = Login( + configuration=self.configuration, + username='job1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr='1.2.3.4', + user_dict=None + ) + job2 = Login( + configuration=self.configuration, + username='job2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr='5.6.7.8', + user_dict=None + ) + # Populate jobs list + self.configuration.daemon_conf['jobs'] = [job1, job2] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_jobs=['job1', 'job2'] + ) + + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('job1', login_map) + self.assertIn('job2', login_map) + self.assertEqual(login_map['job1'], [job1]) + self.assertEqual(login_map['job2'], [job2]) + + def test_update_login_map_shares(self): + """Verify login_map is updated correctly for changed shares.""" + # Create Login objects + share1 = Login( + configuration=self.configuration, + username='share1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + share2 = Login( + configuration=self.configuration, + username='share2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + # Populate shares list + self.configuration.daemon_conf['shares'] = [share1, share2] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_shares=['share1', 'share2'] + ) + + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('share1', login_map) + self.assertIn('share2', login_map) + self.assertEqual(login_map['share1'], [share1]) + self.assertEqual(login_map['share2'], [share2]) + + def test_update_login_map_jupyter(self): + """Verify login_map is updated correctly for changed jupyter mounts.""" + # Create Login objects + jupyter1 = Login( + configuration=self.configuration, + username='jupyter1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + jupyter2 = Login( + configuration=self.configuration, + username='jupyter2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + # Populate jupyter_mounts list + self.configuration.daemon_conf['jupyter_mounts'] = [jupyter1, jupyter2] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_jupyter=['jupyter1', 'jupyter2'] + ) + + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('jupyter1', login_map) + self.assertIn('jupyter2', login_map) + self.assertEqual(login_map['jupyter1'], [jupyter1]) + self.assertEqual(login_map['jupyter2'], [jupyter2]) + + def test_update_login_map_nonexistent(self): + """Verify login_map is set to empty list for non-existent usernames.""" + # Populate users list with one user + user1 = Login( + configuration=self.configuration, + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + self.configuration.daemon_conf['users'] = [user1] + + # Call the function under test for a non-existent user + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=['nonexistent'] + ) + + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('nonexistent', login_map) + self.assertEqual(login_map['nonexistent'], []) + + def test_update_login_map_empty_lists(self): + """Verify login_map is not changed when changed lists are empty.""" + # Populate users list + user1 = Login( + configuration=self.configuration, + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + self.configuration.daemon_conf['users'] = [user1] + + # Call the function under test with empty lists + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_jobs=[], + changed_shares=[], + changed_jupyter=[] + ) + + # Verify login_map is unchanged (should be empty because we didn't update for 'user1') + login_map = self.configuration.daemon_conf['login_map'] + self.assertEqual(len(login_map), 0) + + def test_update_login_map_lock_handling(self): + """Basic check that creds_lock is acquired and released.""" + # Create a Login object + user1 = Login( + configuration=self.configuration, + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + self.configuration.daemon_conf['users'] = [user1] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=['user1'] + ) + + # Verify login_map was updated (lock must have been acquired/released) + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('user1', login_map) + self.assertEqual(login_map['user1'], [user1]) + + if __name__ == '__main__': unittest.main() From 0f1af2a7793963650460b977872e0bec606c60eb Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 12 Jun 2026 12:53:56 +0200 Subject: [PATCH 30/61] Add add_user_object unit tests. --- tests/test_mig_shared_griddaemons.py | 129 ++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index b9d299a47..00e4af1c2 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -46,8 +46,9 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( - Login, get_creds_changes, get_share_changes, get_job_changes, - login_map_lookup, refresh_share_creds, refresh_user_creds, update_login_map + Login, add_user_object, get_creds_changes, get_job_changes, + get_share_changes, login_map_lookup, refresh_share_creds, + refresh_user_creds, update_login_map ) # Imports required for the unit tests themselves @@ -1705,5 +1706,129 @@ def test_update_login_map_lock_handling(self): self.assertEqual(login_map['user1'], [user1]) +class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): + """Unit tests for the add_user_object helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up daemon_conf for add_user_object tests.""" + self.configuration.daemon_conf = { + 'users': [], + 'creds_lock': threading.Lock() + } + + def test_add_user_object_appends_login(self): + """Verify add_user_object appends a Login to users.""" + user_dict = {'user_id': TEST_USER_DN} + + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1', + password=TEST_USER_PW_HASH, + access=READ_WRITE_ACCESS, + user_dict=user_dict + ) + + self.assertEqual(len(self.configuration.daemon_conf['users']), 1) + login = self.configuration.daemon_conf['users'][0] + self.assertIsInstance(login, Login) + self.assertEqual(login.username, TEST_USER_EMAIL) + self.assertEqual(login.home, 'home1') + self.assertEqual(login.password, TEST_USER_PW_HASH) + self.assertIsNone(login.digest) + self.assertIsNone(login.public_key) + self.assertTrue(login.chroot) + self.assertEqual(login.access, READ_WRITE_ACCESS) + self.assertEqual(login.user_dict, user_dict) + + def test_add_user_object_adds_public_key_login(self): + """Verify add_user_object stores parsed public key logins.""" + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1', + pubkey=TEST_USER_PUB_KEY, + access=READ_WRITE_ACCESS + ) + + login = self.configuration.daemon_conf['users'][0] + self.assertIsNotNone(login.public_key) + key_line = "%s %s" % (login.public_key.get_name(), + login.public_key.get_base64()) + self.assertEqual(key_line, TEST_USER_PUB_KEY) + + def test_add_user_object_adds_digest_login(self): + """Verify add_user_object stores digest logins.""" + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1', + digest='digest-value', + access=READ_ONLY_ACCESS + ) + + login = self.configuration.daemon_conf['users'][0] + self.assertEqual(login.digest, 'digest-value') + self.assertIsNone(login.password) + self.assertIsNone(login.public_key) + self.assertEqual(login.access, READ_ONLY_ACCESS) + + def test_add_user_object_without_lock(self): + """Verify add_user_object works when creds_lock is not configured.""" + daemon_conf = self.configuration.daemon_conf + daemon_conf.pop('creds_lock') + + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1' + ) + + self.assertEqual(len(daemon_conf['users']), 1) + self.assertEqual(daemon_conf['users'][0].username, TEST_USER_EMAIL) + + def test_add_user_object_uses_creds_lock(self): + """Verify add_user_object acquires and releases creds_lock.""" + class FakeLock(object): + """Small lock double for add_user_object tests.""" + + def __init__(self): + self.acquired = False + self.released = False + + def acquire(self): + self.acquired = True + + def release(self): + self.released = True + + fake_lock = FakeLock() + self.configuration.daemon_conf['creds_lock'] = fake_lock + + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1' + ) + + self.assertTrue(fake_lock.acquired) + self.assertTrue(fake_lock.released) + + def test_add_user_object_uses_login_as_home_when_home_none(self): + """Verify add_user_object normalizes missing home to login name.""" + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home=None + ) + + login = self.configuration.daemon_conf['users'][0] + self.assertEqual(login.home, TEST_USER_EMAIL) + + if __name__ == '__main__': unittest.main() From deea53f18b7583ce2160bf7ec9bc0f4339145f0f Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 12 Jun 2026 13:09:39 +0200 Subject: [PATCH 31/61] Add add_share_object unit tests. --- tests/test_mig_shared_griddaemons.py | 156 ++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 2 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 00e4af1c2..dcb23acd3 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -46,8 +46,8 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( - Login, add_user_object, get_creds_changes, get_job_changes, - get_share_changes, login_map_lookup, refresh_share_creds, + Login, add_share_object, add_user_object, get_creds_changes, + get_job_changes, get_share_changes, login_map_lookup, refresh_share_creds, refresh_user_creds, update_login_map ) @@ -1830,5 +1830,157 @@ def test_add_user_object_uses_login_as_home_when_home_none(self): self.assertEqual(login.home, TEST_USER_EMAIL) +class MigSharedGriddaemonsLogin__add_share_object(MigTestCase): + """Unit tests for the add_share_object helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up daemon_conf for add_share_object tests.""" + self.configuration.daemon_conf = { + 'shares': [], + 'creds_lock': threading.Lock() + } + + def test_add_share_object_appends_login(self): + """Verify add_share_object appends a Login to shares.""" + user_dict = {} + home_path = temppath('share_home', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS, + user_dict=user_dict + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path, + password=login.password, + access=login.access + ) + self.assertEqual(len(self.configuration.daemon_conf['shares']), 1) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.username, TEST_RW_SHARE_ID) + self.assertEqual(share_login.home, home_path) + self.assertEqual(share_login.password, TEST_RW_SHARE_ID) + self.assertTrue(share_login.chroot) + self.assertEqual(share_login.access, READ_WRITE_ACCESS) + + def test_add_share_object_adds_public_key_login(self): + """Verify add_share_object stores parsed public key logins.""" + home_path = temppath('share_home_pubkey', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS + ) + add_share_object( + configuration=self.configuration, + login=login, + home=home_path, + pubkey=TEST_USER_PUB_KEY + ) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertIsNotNone(share_login.public_key) + key_line = "%s %s" % (share_login.public_key.get_name(), + share_login.public_key.get_base64()) + self.assertEqual(key_line, TEST_USER_PUB_KEY) + + def test_add_share_object_adds_digest_login(self): + """Verify add_share_object stores digest logins.""" + home_path = temppath('share_home_digest', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RO_SHARE_ID, + home=home_path, + password=TEST_RO_SHARE_ID, + access=READ_ONLY_ACCESS + ) + add_share_object( + configuration=self.configuration, + login=login, + home=home_path, + digest='digest-value', + access=READ_ONLY_ACCESS + ) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.digest, 'digest-value') + self.assertIsNone(share_login.password) + self.assertIsNone(share_login.public_key) + self.assertEqual(share_login.access, READ_ONLY_ACCESS) + + def test_add_share_object_without_lock(self): + """Verify add_share_object works when creds_lock is not configured.""" + daemon_conf = self.configuration.daemon_conf + daemon_conf.pop('creds_lock') + home_path = temppath('share_home_nolock', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path) + self.assertEqual(len(daemon_conf['shares']), 1) + self.assertEqual(daemon_conf['shares'][0].username, TEST_RW_SHARE_ID) + + def test_add_share_object_uses_creds_lock(self): + """Verify add_share_object acquires and releases creds_lock.""" + class FakeLock(object): + def __init__(self): + self.acquired = False + self.released = False + + def acquire(self): + self.acquired = True + + def release(self): + self.released = True + fake_lock = FakeLock() + self.configuration.daemon_conf['creds_lock'] = fake_lock + home_path = temppath('share_home_lock', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path) + self.assertTrue(fake_lock.acquired) + self.assertTrue(fake_lock.released) + + def test_add_share_object_uses_login_as_home_when_home_none(self): + """Verify add_share_object normalizes missing home to login name.""" + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=None, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=None) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.home, TEST_RW_SHARE_ID) + + if __name__ == '__main__': unittest.main() From ef855989d1ab8496569e1b7e5725b09dcef4e849 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Sat, 13 Jun 2026 14:03:41 +0200 Subject: [PATCH 32/61] Add update_user_objects unit tests. --- tests/test_mig_shared_griddaemons.py | 190 ++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 2 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index dcb23acd3..62fa86ba0 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -48,7 +48,7 @@ from mig.shared.griddaemons.login import ( Login, add_share_object, add_user_object, get_creds_changes, get_job_changes, get_share_changes, login_map_lookup, refresh_share_creds, - refresh_user_creds, update_login_map + refresh_user_creds, update_login_map, update_user_objects ) # Imports required for the unit tests themselves @@ -127,6 +127,11 @@ def _prepare_auth_files(home_path, auth_protos=None): return auth_files +def _parse_pkey_to_openssh_format(paramiko_pkey): + """Convert a parsed pub key on paramiko PKey format to openssh format""" + return "%s %s" % (paramiko_pkey.get_name(), paramiko_pkey.get_base64()) + + class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): """Unit tests for griddaemons login get_creds_changes function""" @@ -709,7 +714,7 @@ def test_refresh_share_creds_adds_new_share_with_key(self): # self.assertEqual(share_login[0].password, share_pw_hash) # Convert saved paramiko.PKey back to openssh pub key format and check login_key = share_login[1].public_key - result = "%s %s" % (login_key.get_name(), login_key.get_base64()) + result = _parse_pkey_to_openssh_format(login_key) self.assertEqual(result, TEST_USER_PUB_KEY) def test_refresh_share_creds_no_changes(self): @@ -1982,5 +1987,186 @@ def test_add_share_object_uses_login_as_home_when_home_none(self): self.assertEqual(share_login.home, TEST_RW_SHARE_ID) +class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): + """Unit tests for the update_user_objects helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Ensure required directories exist + _ensure_dirs_needed_for_userdb(self.configuration) + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = os.path.join( + self.configuration.user_db_home, "MiG-users.db") + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + + # Create a test user home + self.test_user_home = self._provision_test_user(self, TEST_USER_DN) + self.test_user_dir = os.path.basename(self.test_user_home) + + # Create auth files for the user + self.ssh_auth_paths = _prepare_auth_files(self.test_user_home, ['ssh']) + self.ssh_auth_keys_path, self.ssh_auth_pw_path = self.ssh_auth_paths + + def test_update_user_objects_adds_passwords(self): + """Verify that update_user_objects adds Login objects for passwords.""" + daemon_conf = self.configuration.daemon_conf + # Extract the .PROTO/authorized_keys part from auth_keys_path + authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') + authkeys = authkeys.lstrip(os.sep) + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = authpw.lstrip(os.sep) + authprotos = (authkeys, authpw, None) + user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, + TEST_USER_SHORT_ID, TEST_USER_EMAIL) + # Call the helper + update_user_objects( + configuration=self.configuration, + auth_file=authpw, + path=self.ssh_auth_pw_path, + user_vars=user_tuple, + auth_protos=authprotos, + private_auth_file=True + ) + + # Verify that three aliased pw logins were added + pw_logins = [u for u in daemon_conf['users'] if u.password is not None] + self.assertEqual(len(pw_logins), 3) + for entry in pw_logins: + self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, + TEST_USER_SHORT_ID)) + self.assertEqual(entry.home, self.test_user_dir) + self.assertEqual(entry.access, READ_WRITE_ACCESS) + self.assertEqual(entry.password, TEST_USER_PW_HASH) + + # Verify that no key logins were added + key_logins = [u for u in daemon_conf['users'] + if u.public_key is not None] + self.assertEqual(len(key_logins), 0) + # Verify that no digest logins were added + digest_logins = [u for u in daemon_conf['users'] + if u.digest is not None] + self.assertEqual(len(digest_logins), 0) + + def test_update_user_objects_adds_keys(self): + """Verify that update_user_objects adds Login objects for keys.""" + daemon_conf = self.configuration.daemon_conf + # Extract the .PROTO/authorized_keys part from auth_keys_path + authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') + authkeys = authkeys.lstrip(os.sep) + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = authpw.lstrip(os.sep) + authprotos = (authkeys, authpw, None) + user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, + TEST_USER_SHORT_ID, TEST_USER_EMAIL) + # Call the helper + update_user_objects( + configuration=self.configuration, + auth_file=authkeys, + path=self.ssh_auth_keys_path, + user_vars=user_tuple, + auth_protos=authprotos, + private_auth_file=True + ) + + # Verify that three aliased key logins were added (dupe keys ignored) + key_logins = [u for u in daemon_conf['users'] if u.public_key is not + None] + self.assertEqual(len(key_logins), 3) + + for entry in key_logins: + self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, + TEST_USER_SHORT_ID)) + self.assertEqual(entry.home, self.test_user_dir) + self.assertEqual(entry.access, READ_WRITE_ACCESS) + login_key = entry.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) + + # Verify that no password logins were added + pw_logins = [u for u in daemon_conf['users'] if u.password is not None] + self.assertEqual(len(pw_logins), 0) + # Verify that no digest logins were added + digest_logins = [u for u in daemon_conf['users'] + if u.digest is not None] + self.assertEqual(len(digest_logins), 0) + + def test_update_user_objects_removes_old_entries(self): + """Verify that update_user_objects cleans old entries for the same user.""" + daemon_conf = self.configuration.daemon_conf + # Extract the .PROTO/authorized_passwords part from auth_pw_path + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = authpw.lstrip(os.sep) + authprotos = (None, authpw, None) + user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, + None, None) + # Create a dummy user with a last_update in the past to remove in test + past_timestamp = time.time() - 3600 + dummy_user = Login( + configuration=self.configuration, + username=TEST_USER_EMAIL, + home=self.test_user_dir, + password=TEST_USER_PW_HASH, + access=READ_WRITE_ACCESS + ) + dummy_user.last_update = past_timestamp + daemon_conf['users'].append(dummy_user) + # Remove saved password + os.remove(self.ssh_auth_pw_path) + + # Call the helper + update_user_objects( + configuration=self.configuration, + auth_file=authpw, + path=self.ssh_auth_pw_path, + user_vars=user_tuple, + auth_protos=authprotos, + private_auth_file=True + ) + + # Old entry should be removed + usernames = [u.username for u in daemon_conf['users']] + self.assertNotIn(TEST_USER_DN, usernames, "Old entry was not removed") + + def test_update_user_objects_no_changes(self): + """Verify that calling update_user_objects with unchanged files does not duplicate entries.""" + daemon_conf = self.configuration.daemon_conf + # Extract the .PROTO/authorized_passwords part from auth_pw_path + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = authpw.lstrip(os.sep) + authprotos = (None, authpw, None) + user_tuple = (TEST_USER_DN, None, self.test_user_dir, + None, None) + # First call to populate + update_user_objects( + configuration=self.configuration, + auth_file=authpw, + path=self.ssh_auth_pw_path, + user_vars=user_tuple, + auth_protos=authprotos, + private_auth_file=True + ) + initial_count = len(daemon_conf['users']) + + # Second call with same files + update_user_objects( + configuration=self.configuration, + auth_file=authpw, + path=self.ssh_auth_pw_path, + user_vars=user_tuple, + auth_protos=authprotos, + private_auth_file=True + ) + self.assertEqual(len(daemon_conf['users']), initial_count, + "Duplicate entries were added on second call") + + if __name__ == '__main__': unittest.main() From a7eb90d83680c78a3d3b3b513a3670f79a5891b6 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 09:55:59 +0200 Subject: [PATCH 33/61] Add unit tests for add_jupyter_object and add_jupyter_object . Begin reordering of test classes to fit import sort order. --- tests/test_mig_shared_griddaemons.py | 120 ++++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index 62fa86ba0..e76af0e00 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -46,9 +46,10 @@ # Imports of the code under test from mig.shared.griddaemons.login import ( - Login, add_share_object, add_user_object, get_creds_changes, - get_job_changes, get_share_changes, login_map_lookup, refresh_share_creds, - refresh_user_creds, update_login_map, update_user_objects + Login, add_job_object, add_jupyter_object, add_share_object, + add_user_object, get_creds_changes, get_job_changes, get_share_changes, + login_map_lookup, refresh_share_creds, refresh_user_creds, + update_login_map, update_user_objects ) # Imports required for the unit tests themselves @@ -1711,6 +1712,119 @@ def test_update_login_map_lock_handling(self): self.assertEqual(login_map['user1'], [user1]) +class MigSharedGriddaemonsLogin__add_job_object(MigTestCase): + """Unit tests for the griddaemons add_job_object helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Initialize daemon_conf with empty jobs list + self.configuration.daemon_conf = { + 'jobs': [], + 'creds_lock': threading.Lock() # Ensure lock is present for testing + } + + def test_add_job_object_basic(self): + """Verify that add_job_object adds a Login object to the jobs list.""" + # Call the helper + add_job_object( + configuration=self.configuration, + login='job1', + home='home1', + password=None, + pubkey=TEST_USER_PUB_KEY + ) + + # Verify the job was added + jobs = self.configuration.daemon_conf['jobs'] + self.assertEqual(len(jobs), 1) + job = jobs[0] + self.assertEqual(job.username, 'job1') + self.assertEqual(job.home, 'home1') + self.assertEqual(job.password, None) + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = job.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) + + def test_add_job_object_with_lock(self): + """Verify that add_job_object acquires and releases the creds_lock.""" + # Call the helper + add_job_object( + configuration=self.configuration, + login='job2', + home='home2' + ) + + # Check lock state (simplified verification) + lock = self.configuration.daemon_conf['creds_lock'] + # Note: Direct lock state inspection is not ideal in unit tests + # This test assumes the lock is properly handled by the function + + def test_add_job_object_missing_home(self): + """Verify that add_job_object uses login as home if home is None.""" + add_job_object( + configuration=self.configuration, + login='job3', + home=None + ) + job = self.configuration.daemon_conf['jobs'][0] + self.assertEqual(job.home, 'job3') + + +class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): + """Unit tests for the griddaemons add_jupyter_object helper.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Initialize daemon_conf with empty jupyter_mounts list + self.configuration.daemon_conf = { + 'jupyter_mounts': [], + 'creds_lock': threading.Lock() + } + + def test_add_jupyter_object_basic(self): + """Verify that add_jupyter_object adds a Login object to jupyter_mounts.""" + # Call the helper + add_jupyter_object( + configuration=self.configuration, + login='jupyter1', + home='jupyter_home1', + pubkey=TEST_USER_PUB_KEY + ) + + # Verify the jupyter mount was added + mounts = self.configuration.daemon_conf['jupyter_mounts'] + self.assertEqual(len(mounts), 1) + mount = mounts[0] + self.assertEqual(mount.username, 'jupyter1') + self.assertEqual(mount.home, 'jupyter_home1') + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = mount.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) + + def test_add_jupyter_object_with_lock(self): + """Verify that add_jupyter_object handles the creds_lock.""" + # Call the helper + add_jupyter_object( + configuration=self.configuration, + login='jupyter2', + home='jupyter_home2' + ) + + # Check lock state (simplified verification) + lock = self.configuration.daemon_conf['creds_lock'] + # Assumes the function properly acquires/releases the lock + + class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): """Unit tests for the add_user_object helper.""" From 0f00a25fc8cc9a993ff203579725159868cbcea1 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 10:17:12 +0200 Subject: [PATCH 34/61] Sort unit test classes and refactor out the duplicate FakeLock class. Sync the add_X_object lock tests to actually test lock reusing same structure. --- tests/test_mig_shared_griddaemons.py | 3066 +++++++++++++------------- 1 file changed, 1524 insertions(+), 1542 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index e76af0e00..f4a515639 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -133,420 +133,417 @@ def _parse_pkey_to_openssh_format(paramiko_pkey): return "%s %s" % (paramiko_pkey.get_name(), paramiko_pkey.get_base64()) -class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): - """Unit tests for griddaemons login get_creds_changes function""" +class _FakeLock(object): + """Small lock double for various tests with optional locking.""" - def _provide_configuration(self): - """Return a test configuration instance""" - return 'testconfig' + def __init__(self): + self.acquired = False + self.released = False - def before_each(self): - """Set up test configuration and reset state before each test""" - # Force X509 user id format - self.configuration.site_user_id_format = X509_USER_ID_FORMAT + def acquire(self): + self.acquired = True - _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) + def release(self): + self.released = True - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) - self.auth_keys_path, self.auth_passwords_path = auth_files - self.auth_digests_path = None +class MigSharedGriddaemonsLogin__add_job_object(MigTestCase): + """Unit tests for the griddaemons add_job_object helper.""" - def test_get_creds_changes_detects_new_files(self): - """Verify that new credential files are detected as changes""" - daemon_conf = self.configuration.daemon_conf + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' - # Create a dummy user with a last_update in the past - past_timestamp = time.time() - 3600 - dummy_user = Login( - configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Initialize daemon_conf with empty jobs list + self.configuration.daemon_conf = { + 'jobs': [], + 'creds_lock': threading.Lock() # Ensure lock is present for testing + } - changed_paths = get_creds_changes( - daemon_conf, - 'user', - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path + def test_add_job_object_basic(self): + """Verify that add_job_object adds a Login object to the jobs list.""" + # Call the helper + add_job_object( + configuration=self.configuration, + login='job1', + home='home1', + password=None, + pubkey=TEST_USER_PUB_KEY ) - self.assertIn(self.auth_keys_path, changed_paths) - self.assertIn(self.auth_passwords_path, changed_paths) - # self.assertIn(self.auth_digests_path, changed_paths) - - def test_get_creds_changes_no_changes(self): - """Verify that unchanged credential files return an empty list""" - daemon_conf = self.configuration.daemon_conf + # Verify the job was added + jobs = self.configuration.daemon_conf['jobs'] + self.assertEqual(len(jobs), 1) + job = jobs[0] + self.assertEqual(job.username, 'job1') + self.assertEqual(job.home, 'home1') + self.assertEqual(job.password, None) + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = job.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) - # Set the file modification times to now - current_time = time.time() + def test_add_job_object_uses_creds_lock(self): + """Verify add_job_object acquires and releases creds_lock.""" + fake_lock = _FakeLock() + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_job_object(configuration=self.configuration, + login='job2', + home='job_home2') + self.assertTrue(fake_lock.acquired) + self.assertTrue(fake_lock.released) - # Create a dummy user with last_update matching the file mtime - dummy_user = Login( + def test_add_job_object_missing_home(self): + """Verify that add_job_object uses login as home if home is None.""" + add_job_object( configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = current_time - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - TEST_USER_SHORT_ID, - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path + login='job3', + home=None ) - - self.assertEqual(len(changed_paths), 0) + job = self.configuration.daemon_conf['jobs'][0] + self.assertEqual(job.home, 'job3') -class MigSharedGriddaemonsLogin__get_creds_changes_uuid_user_id(MigTestCase): - """Unit tests for griddaemons login get_creds_changes function with UUID users""" +class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): + """Unit tests for the griddaemons add_jupyter_object helper.""" def _provide_configuration(self): - """Return a test configuration instance""" + """Return a test configuration instance.""" return 'testconfig' def before_each(self): - """Set up test configuration and reset state before each test""" - # Force UUID user id format - self.configuration.site_user_id_format = UUID_USER_ID_FORMAT - # Ensure required directories exist - ensure_dirs_exist(self.configuration.mig_system_files) - _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - - user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) - self.test_user_id = user_id = user_dict['unique_id'] - client_dir = client_id_dir(TEST_USER_DN) - self.test_user_home = os.path.join(self.configuration.user_home, - user_id) - self.test_user_dir = os.path.basename(self.test_user_home) - self.test_user_home_x509 = os.path.join(self.configuration.user_home, - client_dir) - self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) - # Make sure X509 link are provisioned as well - if not os.path.islink(self.test_user_home_x509): - os.symlink(self.test_user_home, self.test_user_home_x509) - - auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) - self.auth_keys_path, self.auth_passwords_path = auth_files - self.auth_digests_path = None - - def test_get_creds_changes_detects_new_files(self): - """Verify that new credential files are detected as changes""" - daemon_conf = self.configuration.daemon_conf + """Set up test configuration and reset state before each test.""" + # Initialize daemon_conf with empty jupyter_mounts list + self.configuration.daemon_conf = { + 'jupyter_mounts': [], + 'creds_lock': threading.Lock() + } - # Create a dummy user with a last_update in the past - past_timestamp = time.time() - 3600 - # NOTE: we use x509 alias over uuid home as in real runs - dummy_user = Login( + def test_add_jupyter_object_basic(self): + """Verify that add_jupyter_object adds a Login object to jupyter_mounts.""" + # Call the helper + add_jupyter_object( configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home_x509, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - 'user', - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path + login='jupyter1', + home='jupyter_home1', + pubkey=TEST_USER_PUB_KEY ) - self.assertIn(self.auth_keys_path, changed_paths) - self.assertIn(self.auth_passwords_path, changed_paths) - # self.assertIn(self.auth_digests_path, changed_paths) - - def test_get_creds_changes_no_changes(self): - """Verify that unchanged credential files return an empty list""" - daemon_conf = self.configuration.daemon_conf + # Verify the jupyter mount was added + mounts = self.configuration.daemon_conf['jupyter_mounts'] + self.assertEqual(len(mounts), 1) + mount = mounts[0] + self.assertEqual(mount.username, 'jupyter1') + self.assertEqual(mount.home, 'jupyter_home1') + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = mount.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) - # Set the file modification times to now - current_time = time.time() + def test_add_jupyter_object_uses_creds_lock(self): + """Verify add_jupyter_object acquires and releases creds_lock.""" + fake_lock = _FakeLock() + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_jupyter_object(configuration=self.configuration, + login='jupyter2', + home='jupyter_home2') + self.assertTrue(fake_lock.acquired) + self.assertTrue(fake_lock.released) - # Create a dummy user with last_update matching the file mtime - # NOTE: we use x509 alias over uuid home as in real runs - dummy_user = Login( + def test_add_jupyter_object_missing_home(self): + """Verify that add_jupyter_object uses login as home if home is None.""" + add_jupyter_object( configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home_x509, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = current_time - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - TEST_USER_SHORT_ID, - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path + login='jupyter3', + home=None ) - - self.assertEqual(len(changed_paths), 0) + mount = self.configuration.daemon_conf['jupyter_mounts'][0] + self.assertEqual(mount.home, 'jupyter3') -class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): - """Unit tests for griddaemons login get_share_changes helper function""" +class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): + """Unit tests for the add_user_object helper.""" def _provide_configuration(self): - """Return a test configuration instance""" + """Return a test configuration instance.""" return 'testconfig' def before_each(self): - """Set up test configuration and reset state before each test""" - _ensure_dirs_needed_for_userdb(self.configuration) - self.ro_share_home = os.path.join(self.configuration.sharelink_home, - 'read-only') - self.rw_share_home = os.path.join(self.configuration.sharelink_home, - 'read-write') - self.wo_share_home = os.path.join(self.configuration.sharelink_home, - 'write-only') - ensure_dirs_exist(self.ro_share_home) - ensure_dirs_exist(self.rw_share_home) - ensure_dirs_exist(self.wo_share_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['shares'] = [] - - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - # self.auth_digests_path = temppath('authhorized_digests', self) - self.auth_digests_path = None - - # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - # with open(self.auth_digests_path, 'w') as creds_fd: - # creds_fd.write(TEST_USER_DIGEST) - - def test_get_share_changes_detects_updates(self): - """Verify that share link and key file changes are detected""" - daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True + """Set up daemon_conf for add_user_object tests.""" + self.configuration.daemon_conf = { + 'users': [], + 'creds_lock': threading.Lock() + } - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') - ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join(user_shared_dir, '.ssh', - 'authorized_keys') - share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + def test_add_user_object_appends_login(self): + """Verify add_user_object appends a Login to users.""" + user_dict = {'user_id': TEST_USER_DN} - # Create a dummy share with a last_update in the past - past_timestamp = time.time() - 3600 - dummy_share = Login( + add_user_object( configuration=self.configuration, - username=TEST_RO_SHARE_ID, - home='share_home', + login=TEST_USER_EMAIL, + home='home1', password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_share.last_update = past_timestamp - daemon_conf['shares'].append(dummy_share) - - changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - user_shared_keys + access=READ_WRITE_ACCESS, + user_dict=user_dict ) - self.assertIn(share_link_path, changed_paths) - self.assertIn(user_shared_keys, changed_paths) + self.assertEqual(len(self.configuration.daemon_conf['users']), 1) + login = self.configuration.daemon_conf['users'][0] + self.assertIsInstance(login, Login) + self.assertEqual(login.username, TEST_USER_EMAIL) + self.assertEqual(login.home, 'home1') + self.assertEqual(login.password, TEST_USER_PW_HASH) + self.assertIsNone(login.digest) + self.assertIsNone(login.public_key) + self.assertTrue(login.chroot) + self.assertEqual(login.access, READ_WRITE_ACCESS) + self.assertEqual(login.user_dict, user_dict) - def test_get_share_changes_new_share(self): - """Verify that a new share link is detected as a change""" - daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True + def test_add_user_object_adds_public_key_login(self): + """Verify add_user_object stores parsed public key logins.""" + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1', + pubkey=TEST_USER_PUB_KEY, + access=READ_WRITE_ACCESS + ) - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') - ensure_dirs_exist(user_shared_dir) - share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + login = self.configuration.daemon_conf['users'][0] + self.assertIsNotNone(login.public_key) + key_line = "%s %s" % (login.public_key.get_name(), + login.public_key.get_base64()) + self.assertEqual(key_line, TEST_USER_PUB_KEY) - changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - self.auth_keys_path + def test_add_user_object_adds_digest_login(self): + """Verify add_user_object stores digest logins.""" + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home='home1', + digest='digest-value', + access=READ_ONLY_ACCESS ) - self.assertIn(share_link_path, changed_paths) - self.assertIn(self.auth_keys_path, changed_paths) + login = self.configuration.daemon_conf['users'][0] + self.assertEqual(login.digest, 'digest-value') + self.assertIsNone(login.password) + self.assertIsNone(login.public_key) + self.assertEqual(login.access, READ_ONLY_ACCESS) - def test_get_share_changes_no_changes(self): - """Verify that unchanged share files return an empty list""" + def test_add_user_object_without_lock(self): + """Verify add_user_object works when creds_lock is not configured.""" daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True - - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') - ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join(user_shared_dir, '.ssh', - 'authorized_keys') - ensure_dirs_exist(user_shared_keys) - share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + daemon_conf.pop('creds_lock') - # Create a dummy share with last_update matching file mtime - current_time = time.time() - dummy_share = Login( + add_user_object( configuration=self.configuration, - username=TEST_RO_SHARE_ID, - home='share_home', - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + login=TEST_USER_EMAIL, + home='home1' ) - dummy_share.last_update = current_time - daemon_conf['shares'].append(dummy_share) - changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - user_shared_keys + self.assertEqual(len(daemon_conf['users']), 1) + self.assertEqual(daemon_conf['users'][0].username, TEST_USER_EMAIL) + + def test_add_user_object_uses_creds_lock(self): + """Verify add_user_object acquires and releases creds_lock.""" + fake_lock = _FakeLock() + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_user_object(configuration=self.configuration, + login=TEST_USER_EMAIL, + home='user_home2' + ) + self.assertTrue(fake_lock.acquired) + self.assertTrue(fake_lock.released) + + def test_add_user_object_uses_login_as_home_when_home_none(self): + """Verify add_user_object normalizes missing home to login name.""" + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home=None ) - self.assertEqual(len(changed_paths), 0) + login = self.configuration.daemon_conf['users'][0] + self.assertEqual(login.home, TEST_USER_EMAIL) -class MigSharedGriddaemonsLogin__get_job_changes(MigTestCase): - """Unit tests for griddaemons login get_job_changes function""" +class MigSharedGriddaemonsLogin__add_share_object(MigTestCase): + """Unit tests for the add_share_object helper.""" def _provide_configuration(self): - """Return a test configuration instance""" + """Return a test configuration instance.""" return 'testconfig' def before_each(self): - """Set up test configuration and reset state before each test""" - _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) + """Set up daemon_conf for add_share_object tests.""" + self.configuration.daemon_conf = { + 'shares': [], + 'creds_lock': threading.Lock() + } - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['jobs'] = [] + def test_add_share_object_appends_login(self): + """Verify add_share_object appends a Login to shares.""" + user_dict = {} + home_path = temppath('share_home', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS, + user_dict=user_dict + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path, + password=login.password, + access=login.access + ) + self.assertEqual(len(self.configuration.daemon_conf['shares']), 1) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.username, TEST_RW_SHARE_ID) + self.assertEqual(share_login.home, home_path) + self.assertEqual(share_login.password, TEST_RW_SHARE_ID) + self.assertTrue(share_login.chroot) + self.assertEqual(share_login.access, READ_WRITE_ACCESS) - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False + def test_add_share_object_adds_public_key_login(self): + """Verify add_share_object stores parsed public key logins.""" + home_path = temppath('share_home_pubkey', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS + ) + add_share_object( + configuration=self.configuration, + login=login, + home=home_path, + pubkey=TEST_USER_PUB_KEY + ) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertIsNotNone(share_login.public_key) + key_line = "%s %s" % (share_login.public_key.get_name(), + share_login.public_key.get_base64()) + self.assertEqual(key_line, TEST_USER_PUB_KEY) - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - # self.auth_digests_path = temppath('authhorized_digests', self) - self.auth_digests_path = None - - # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - # with open(self.auth_digests_path, 'w') as creds_fd: - # creds_fd.write(TEST_USER_DIGEST) + def test_add_share_object_adds_digest_login(self): + """Verify add_share_object stores digest logins.""" + home_path = temppath('share_home_digest', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RO_SHARE_ID, + home=home_path, + password=TEST_RO_SHARE_ID, + access=READ_ONLY_ACCESS + ) + add_share_object( + configuration=self.configuration, + login=login, + home=home_path, + digest='digest-value', + access=READ_ONLY_ACCESS + ) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.digest, 'digest-value') + self.assertIsNone(share_login.password) + self.assertIsNone(share_login.public_key) + self.assertEqual(share_login.access, READ_ONLY_ACCESS) - def test_get_job_changes_new_job(self): - """Verify that a new job mrsl file is detected as a change""" + def test_add_share_object_without_lock(self): + """Verify add_share_object works when creds_lock is not configured.""" daemon_conf = self.configuration.daemon_conf + daemon_conf.pop('creds_lock') + home_path = temppath('share_home_nolock', self) + ensure_dirs_exist(home_path) + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=home_path, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path) + self.assertEqual(len(daemon_conf['shares']), 1) + self.assertEqual(daemon_conf['shares'][0].username, TEST_RW_SHARE_ID) - mrsl_path = temppath('test_job.mRSL', self) - with open(mrsl_path, 'w') as mrsl_fd: - mrsl_fd.write('test content') + def test_add_share_object_uses_creds_lock(self): + """Verify add_share_object acquires and releases creds_lock.""" + fake_lock = _FakeLock() + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_share_object(configuration=self.configuration, + login=TEST_RW_SHARE_ID, + home='share_home2') + self.assertTrue(fake_lock.acquired) + self.assertTrue(fake_lock.released) - changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path + def test_add_share_object_uses_login_as_home_when_home_none(self): + """Verify add_share_object normalizes missing home to login name.""" + login = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=None, + password=TEST_RW_SHARE_ID, + access=READ_WRITE_ACCESS ) + add_share_object(configuration=self.configuration, + login=login.username, + home=None) + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.home, TEST_RW_SHARE_ID) - self.assertIn(mrsl_path, changed_paths) - def test_get_job_changes_no_changes(self): - """Verify that unchanged job mrsl file returns an empty list""" - daemon_conf = self.configuration.daemon_conf +class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): + """Unit tests for griddaemons login get_creds_changes function""" - mrsl_path = temppath('test_job.mRSL', self) - with open(mrsl_path, 'w') as mrsl_fd: - mrsl_fd.write('test content') + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' - # Create a dummy job with last_update matching file mtime - current_time = time.time() - dummy_job = Login( + def before_each(self): + """Set up test configuration and reset state before each test""" + # Force X509 user id format + self.configuration.site_user_id_format = X509_USER_ID_FORMAT + + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False + + self.test_user_home = self._provision_test_user(self, TEST_USER_DN) + auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + self.auth_keys_path, self.auth_passwords_path = auth_files + self.auth_digests_path = None + + def test_get_creds_changes_detects_new_files(self): + """Verify that new credential files are detected as changes""" + daemon_conf = self.configuration.daemon_conf + + # Create a dummy user with a last_update in the past + past_timestamp = time.time() - 3600 + dummy_user = Login( configuration=self.configuration, - username='test_session_id', - home='job_home', - password=None, + username=TEST_USER_SHORT_ID, + home=self.test_user_home, + password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, @@ -554,27 +551,34 @@ def test_get_job_changes_no_changes(self): ip_addr=None, user_dict=None ) - dummy_job.last_update = current_time - daemon_conf['jobs'].append(dummy_job) + dummy_user.last_update = past_timestamp + daemon_conf['users'].append(dummy_user) - changed_paths = get_job_changes( + changed_paths = get_creds_changes( daemon_conf, - 'test_session_id', - mrsl_path + 'user', + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path ) - self.assertEqual(len(changed_paths), 0) + self.assertIn(self.auth_keys_path, changed_paths) + self.assertIn(self.auth_passwords_path, changed_paths) + # self.assertIn(self.auth_digests_path, changed_paths) - def test_get_job_changes_missing_file(self): - """Verify that missing mrsl file is detected for existing job""" + def test_get_creds_changes_no_changes(self): + """Verify that unchanged credential files return an empty list""" daemon_conf = self.configuration.daemon_conf - # Create a dummy job - dummy_job = Login( + # Set the file modification times to now + current_time = time.time() + + # Create a dummy user with last_update matching the file mtime + dummy_user = Login( configuration=self.configuration, - username='test_session_id', - home='job_home', - password=None, + username=TEST_USER_SHORT_ID, + home=self.test_user_home, + password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, @@ -582,536 +586,486 @@ def test_get_job_changes_missing_file(self): ip_addr=None, user_dict=None ) - daemon_conf['jobs'].append(dummy_job) - - mrsl_path = temppath('missing_job.mRSL', self) + dummy_user.last_update = current_time + daemon_conf['users'].append(dummy_user) - changed_paths = get_job_changes( + changed_paths = get_creds_changes( daemon_conf, - 'test_session_id', - mrsl_path + TEST_USER_SHORT_ID, + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path ) - self.assertIn(mrsl_path, changed_paths) + self.assertEqual(len(changed_paths), 0) -class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): - """Unit tests for the griddaemons login refresh_share_creds helper.""" +class MigSharedGriddaemonsLogin__get_creds_changes_uuid_user_id(MigTestCase): + """Unit tests for griddaemons login get_creds_changes function with UUID users""" def _provide_configuration(self): - """Return a test configuration instance.""" + """Return a test configuration instance""" return 'testconfig' def before_each(self): - """Set up test configuration and reset state before each test.""" - # The base class already creates the required directory layout. - # Ensure the share‑link home exists – it is used by refresh_share_creds. - self.ro_share_home = os.path.join(self.configuration.sharelink_home, - 'read-only') - self.rw_share_home = os.path.join(self.configuration.sharelink_home, - 'read-write') - self.wo_share_home = os.path.join(self.configuration.sharelink_home, - 'write-only') - ensure_dirs_exist(self.ro_share_home) - ensure_dirs_exist(self.rw_share_home) - ensure_dirs_exist(self.wo_share_home) + """Set up test configuration and reset state before each test""" + # Force UUID user id format + self.configuration.site_user_id_format = UUID_USER_ID_FORMAT + # Ensure required directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['users'] = [] self.configuration.daemon_conf['allow_publickey'] = True self.configuration.daemon_conf['allow_password'] = True + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True self.configuration.daemon_conf['allow_digest'] = False - # Paths that the function will look at - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - self.auth_digests_path = None - - # Create dummy credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) + self.test_user_id = user_id = user_dict['unique_id'] + client_dir = client_id_dir(TEST_USER_DN) + self.test_user_home = os.path.join(self.configuration.user_home, + user_id) + self.test_user_dir = os.path.basename(self.test_user_home) + self.test_user_home_x509 = os.path.join(self.configuration.user_home, + client_dir) + self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) + # Make sure X509 link are provisioned as well + if not os.path.islink(self.test_user_home_x509): + os.symlink(self.test_user_home, self.test_user_home_x509) - def test_refresh_share_creds_adds_new_share(self): - """A new share link should be added to daemon_conf['shares'].""" - # Build a share link that points to a temporary user directory - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) + auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + self.auth_keys_path, self.auth_passwords_path = auth_files + self.auth_digests_path = None - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + def test_get_creds_changes_detects_new_files(self): + """Verify that new credential files are detected as changes""" + daemon_conf = self.configuration.daemon_conf - # Call the function under test - # NOTE: only sftp access is supported for now - (updated_conf, changed_shares) = refresh_share_creds( + # Create a dummy user with a last_update in the past + past_timestamp = time.time() - 3600 + # NOTE: we use x509 alias over uuid home as in real runs + dummy_user = Login( configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # The share should now be present in the changed list - self.assertIn(TEST_RW_SHARE_ID, changed_shares) - - # Verify that a Login object was added to shares - share_login = [ - obj for obj in updated_conf['shares'] - if obj.username == TEST_RW_SHARE_ID - ] - self.assertEqual(len(share_login), 1) - - # The added object should contain the expected home directory - self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) - self.assertEqual(share_login[0].home, rel_share_home) - # TODO: check password hash, too? - # share_pw_hash = generate_password_hash(self.configuration, - # TEST_RW_SHARE_ID) - # self.assertEqual(share_login[0].password, share_pw_hash) - - def test_refresh_share_creds_adds_new_share_with_key(self): - """A new share link with key should be added twice to daemon_conf['shares'].""" - # Build a share link that points to a temporary user directory - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - _prepare_auth_files(user_shared_dir, ['ssh']) - - # Call the function under test - # NOTE: only sftp access is supported for now - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + username=TEST_USER_SHORT_ID, + home=self.test_user_home_x509, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + dummy_user.last_update = past_timestamp + daemon_conf['users'].append(dummy_user) - # The share should now be present twice in the changed list - self.assertIn(TEST_RW_SHARE_ID, changed_shares) - - # Verify that a Login object was added to shares - share_login = [ - obj for obj in updated_conf['shares'] - if obj.username == TEST_RW_SHARE_ID - ] - self.assertEqual(len(share_login), 2) + changed_paths = get_creds_changes( + daemon_conf, + 'user', + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path + ) - # The added objects should contain the expected home directory - self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) - self.assertEqual(share_login[0].home, rel_share_home) - self.assertEqual(share_login[1].username, TEST_RW_SHARE_ID) - self.assertEqual(share_login[1].home, rel_share_home) - # TODO: check password hash, too? - # share_pw_hash = generate_password_hash(self.configuration, - # TEST_RW_SHARE_ID) - # self.assertEqual(share_login[0].password, share_pw_hash) - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = share_login[1].public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) + self.assertIn(self.auth_keys_path, changed_paths) + self.assertIn(self.auth_passwords_path, changed_paths) + # self.assertIn(self.auth_digests_path, changed_paths) - def test_refresh_share_creds_no_changes(self): - """When the share link and its key file have not changed, the function - should return an empty changed_shares list.""" + def test_get_creds_changes_no_changes(self): + """Verify that unchanged credential files return an empty list""" daemon_conf = self.configuration.daemon_conf - # Create a share link that already exists - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - # Populate shares with a dummy entry whose last_update matches - # the current file mtime – this simulates “no changes”. + # Set the file modification times to now current_time = time.time() - dummy_share = Login( + + # Create a dummy user with last_update matching the file mtime + # NOTE: we use x509 alias over uuid home as in real runs + dummy_user = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=rel_share_home, - password=TEST_RW_SHARE_ID, + username=TEST_USER_SHORT_ID, + home=self.test_user_home_x509, + password=TEST_USER_PW_HASH, digest=None, - public_key=None, + public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None) - dummy_share.last_update = current_time - daemon_conf['shares'].append(dummy_share) + user_dict=None + ) + dummy_user.last_update = current_time + daemon_conf['users'].append(dummy_user) - # Run the function - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + changed_paths = get_creds_changes( + daemon_conf, + TEST_USER_SHORT_ID, + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path ) - # No changes should be reported - self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(changed_paths), 0) - # The dummy entry should still be the only one present - self.assertEqual(len(updated_conf['shares']), 1) - def test_refresh_share_creds_adds_readonly_share(self): - """Test that a read‑only share is added correctly""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) +class MigSharedGriddaemonsLogin__get_job_changes(MigTestCase): + """Unit tests for griddaemons login get_job_changes function""" - share_link_path = os.path.join(self.configuration.sharelink_home, - 'read-only', TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RO_SHARE_ID, - share_modes=(READ_ONLY_ACCESS, ) - ) + def before_each(self): + """Set up test configuration and reset state before each test""" + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) - self.assertIn(TEST_RO_SHARE_ID, changed_shares) - share_login = [obj for obj in updated_conf['shares'] - if obj.username == TEST_RO_SHARE_ID] - self.assertEqual(len(share_login), 1) - self.assertEqual(share_login[0].home, rel_share_home) + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['jobs'] = [] - def test_refresh_share_creds_adds_writeonly_share(self): - """Test that a write‑only share is added correctly""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False - share_link_path = os.path.join(self.wo_share_home, TEST_WO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + # self.auth_digests_path = temppath('authhorized_digests', self) + self.auth_digests_path = None - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_WO_SHARE_ID, - share_modes=(WRITE_ONLY_ACCESS, ) + # Create sample credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + # with open(self.auth_digests_path, 'w') as creds_fd: + # creds_fd.write(TEST_USER_DIGEST) + + def test_get_job_changes_new_job(self): + """Verify that a new job mrsl file is detected as a change""" + daemon_conf = self.configuration.daemon_conf + + mrsl_path = temppath('test_job.mRSL', self) + with open(mrsl_path, 'w') as mrsl_fd: + mrsl_fd.write('test content') + + changed_paths = get_job_changes( + daemon_conf, + 'test_session_id', + mrsl_path ) - self.assertIn(TEST_WO_SHARE_ID, changed_shares) - share_login = [obj for obj in updated_conf['shares'] - if obj.username == TEST_WO_SHARE_ID] - self.assertEqual(len(share_login), 1) - self.assertEqual(share_login[0].home, rel_share_home) + self.assertIn(mrsl_path, changed_paths) - def test_refresh_share_creds_no_change_on_unchanged_link(self): - """Test that an unchanged share link does not trigger a change""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home) - ensure_dirs_exist(user_shared_dir) + def test_get_job_changes_no_changes(self): + """Verify that unchanged job mrsl file returns an empty list""" + daemon_conf = self.configuration.daemon_conf - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) + mrsl_path = temppath('test_job.mRSL', self) + with open(mrsl_path, 'w') as mrsl_fd: + mrsl_fd.write('test content') - # Populate shares with a dummy entry whose last_update matches - # the current file mtime – this simulates “no changes”. + # Create a dummy job with last_update matching file mtime current_time = time.time() - dummy_share = Login( + dummy_job = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=rel_share_home, - password=TEST_RW_SHARE_ID, + username='test_session_id', + home='job_home', + password=None, digest=None, - public_key=None, + public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None) - dummy_share.last_update = current_time - self.configuration.daemon_conf['shares'].append(dummy_share) + user_dict=None + ) + dummy_job.last_update = current_time + daemon_conf['jobs'].append(dummy_job) - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + changed_paths = get_job_changes( + daemon_conf, + 'test_session_id', + mrsl_path ) - self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 1) - - def test_refresh_share_creds_detects_missing_link(self): - """Test that a missing share link is reported as a change""" - # No symlink created - share link is missing - missing_share_id = 'missing123' - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=missing_share_id, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # The function should still return an empty list because the link - # does not exist; no share is added. - self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 0) + self.assertEqual(len(changed_paths), 0) - def test_refresh_share_creds_ignores_dead_link(self): - """Test that a dead share link is ignored""" - # Create a symlink that points nowhere - invalid_target = os.path.join(self.configuration.user_home, 'deadbeef') - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(invalid_target, share_link_path) + def test_get_job_changes_missing_file(self): + """Verify that missing mrsl file is detected for existing job""" + daemon_conf = self.configuration.daemon_conf - (updated_conf, changed_shares) = refresh_share_creds( + # Create a dummy job + dummy_job = Login( configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + username='test_session_id', + home='job_home', + password=None, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + daemon_conf['jobs'].append(dummy_job) - # No share should be added because the link is invalid - self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 0) - - def test_refresh_share_creds_ignores_invalid_link(self): - """Test that an invalid (out of bounds) share link is ignored""" - # Create a symlink that points outside a user_home directory - invalid_target = self.configuration.certs_path - ensure_dirs_exist(invalid_target) - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(invalid_target, share_link_path) + mrsl_path = temppath('missing_job.mRSL', self) - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + changed_paths = get_job_changes( + daemon_conf, + 'test_session_id', + mrsl_path ) - # No share should be added because the link is invalid but change - # is still reported as modified. - self.assertEqual(len(changed_shares), 1) - self.assertEqual(changed_shares[0], TEST_RW_SHARE_ID) - self.assertEqual(len(updated_conf['shares']), 0) + self.assertIn(mrsl_path, changed_paths) -class MigSharedGriddaemonsLogin__refresh_user_creds(MigTestCase, UserAssertMixin): - """Unit tests for the griddaemons login refresh_user_creds helper.""" +class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): + """Unit tests for griddaemons login get_share_changes helper function""" def _provide_configuration(self): - """Return a test configuration instance.""" + """Return a test configuration instance""" return 'testconfig' def before_each(self): - """Set up test configuration and reset state before each test.""" - # Force X509 user id format - self.configuration.site_user_id_format = X509_USER_ID_FORMAT - # Ensure required directories exist + """Set up test configuration and reset state before each test""" _ensure_dirs_needed_for_userdb(self.configuration) - self.expected_user_db_home = os.path.normpath( - self.configuration.user_db_home - ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" - ) - self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - self.test_user_dir = os.path.basename(self.test_user_home) - # Make sure alias link is provisioned as well - alias_link_path = os.path.join(self.configuration.user_home, - TEST_USER_EMAIL) - if not os.path.islink(alias_link_path): - os.symlink(self.test_user_home, alias_link_path) - - ALIAS_FIELD = 'email' - self.configuration.user_sftp_alias = ALIAS_FIELD - self.configuration.user_ftps_alias = ALIAS_FIELD - self.configuration.user_davs_alias = ALIAS_FIELD + self.ro_share_home = os.path.join(self.configuration.sharelink_home, + 'read-only') + self.rw_share_home = os.path.join(self.configuration.sharelink_home, + 'read-write') + self.wo_share_home = os.path.join(self.configuration.sharelink_home, + 'write-only') + ensure_dirs_exist(self.ro_share_home) + ensure_dirs_exist(self.rw_share_home) + ensure_dirs_exist(self.wo_share_home) - # Common daemon configuration self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = self.expected_user_db_file - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD - - def test_refresh_user_creds_ssh_protocol(self): - """Test refreshing user credentials for SSH protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + self.configuration.daemon_conf['shares'] = [] - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False - # The user should be in the changed list - self.assertIn(username, changed_users) + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + # self.auth_digests_path = temppath('authhorized_digests', self) + self.auth_digests_path = None - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least one login (the main username) and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) + # Create sample credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) + # with open(self.auth_digests_path, 'w') as creds_fd: + # creds_fd.write(TEST_USER_DIGEST) - # Check that at least one login has the correct home directory - home_found = any(login.home == self.test_user_dir for login in - user_logins) - self.assertTrue(home_found) + def test_get_share_changes_detects_updates(self): + """Verify that share link and key file changes are detected""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True - def test_refresh_user_creds_davs_protocol(self): - """Test refreshing user credentials for DAVS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['davs']) + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') + ensure_dirs_exist(user_shared_dir) + user_shared_keys = os.path.join(user_shared_dir, '.ssh', + 'authorized_keys') + share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + # Create a dummy share with a last_update in the past + past_timestamp = time.time() - 3600 + dummy_share = Login( configuration=self.configuration, - protocol='davs', - username=username + username=TEST_RO_SHARE_ID, + home='share_home', + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + dummy_share.last_update = past_timestamp + daemon_conf['shares'].append(dummy_share) - # The user should be in the changed list - self.assertIn(username, changed_users) + changed_paths = get_share_changes( + daemon_conf, + TEST_RO_SHARE_ID, + share_link_path, + user_shared_keys + ) - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - self.assertEqual(user_logins[0].home, self.test_user_dir) + self.assertIn(share_link_path, changed_paths) + self.assertIn(user_shared_keys, changed_paths) - def test_refresh_user_creds_ftps_protocol(self): - """Test refreshing user credentials for FTPS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ftps']) + def test_get_share_changes_new_share(self): + """Verify that a new share link is detected as a change""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ftps', - username=username + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') + ensure_dirs_exist(user_shared_dir) + share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + changed_paths = get_share_changes( + daemon_conf, + TEST_RO_SHARE_ID, + share_link_path, + self.auth_keys_path ) - # The user should be in the changed list - self.assertIn(username, changed_users) + self.assertIn(share_link_path, changed_paths) + self.assertIn(self.auth_keys_path, changed_paths) - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - self.assertEqual(user_logins[0].home, self.test_user_dir) + def test_get_share_changes_no_changes(self): + """Verify that unchanged share files return an empty list""" + daemon_conf = self.configuration.daemon_conf + daemon_conf['allow_publickey'] = True - def test_refresh_user_creds_https_protocol(self): - """Test refreshing user credentials for HTTPS protocol (uses user DB).""" - username = TEST_USER_EMAIL + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') + ensure_dirs_exist(user_shared_dir) + user_shared_keys = os.path.join(user_shared_dir, '.ssh', + 'authorized_keys') + ensure_dirs_exist(user_shared_keys) + share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + # Create a dummy share with last_update matching file mtime + current_time = time.time() + dummy_share = Login( configuration=self.configuration, - protocol='https', - username=username + username=TEST_RO_SHARE_ID, + home='share_home', + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + dummy_share.last_update = current_time + daemon_conf['shares'].append(dummy_share) - # The user alias should be in the changed list - self.assertIn(TEST_USER_EMAIL, changed_users) + changed_paths = get_share_changes( + daemon_conf, + TEST_RO_SHARE_ID, + share_link_path, + user_shared_keys + ) - # Verify that Login objects were added to users for the username and its aliases - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least the main username and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) + self.assertEqual(len(changed_paths), 0) - # Check that at least one login has the correct home directory - home_found = any(login.home == self.test_user_dir for login in - user_logins) - self.assertTrue(home_found) - def test_refresh_user_creds_no_changes(self): - """Test that no changes are reported when credentials are unchanged.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) +class MigSharedGriddaemonsLogin__login_map_lookup(MigTestCase): + """Unit tests for the login_map_lookup function from griddaemons/login.py""" - # Pre-populate the users list with a Login object that has - # last_update set to the current time (simulating no changes) - current_time = time.time() - dummy_user = Login( - configuration=self.configuration, - username=username, - home=self.test_user_home, - password=TEST_USER_PW_HASH, + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration with login_map and creds_lock.""" + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['login_map'] = {} + self.configuration.daemon_conf['creds_lock'] = threading.Lock() + + def test_user_exists_with_multiple_credentials(self): + """Verify login_map_lookup returns all credentials for a user.""" + # Create two Login objects for 'user1' + cred1 = Login( + configuration=self.configuration.daemon_conf, + username='user1', + home='home1', + password=None, digest=None, - public_key=TEST_USER_PUB_KEY, + public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None) - dummy_user.last_update = current_time - self.configuration.daemon_conf['users'].append(dummy_user) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + user_dict=None + ) + cred2 = Login( configuration=self.configuration, - protocol='ssh', - username=username + username='user1', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + # Populate login_map + daemon_conf = self.configuration.daemon_conf + daemon_conf['login_map']['user1'] = [cred1, cred2] + # Call the function + result = login_map_lookup(daemon_conf, 'user1') + # Assert + self.assertEqual(result, [cred1, cred2]) - # No changes should be reported - self.assertEqual(len(changed_users), 0) - # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf['users']), 1) - self.assertEqual(updated_conf['users'][0].username, username) - - def test_refresh_user_creds_missing_user(self): - """Test that a missing user is skipped.""" - username = 'nosuchuser' - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + def test_user_exists_with_single_credential(self): + """Verify login_map_lookup returns one credential for a user.""" + cred = Login( configuration=self.configuration, - protocol='ssh', - username=username + username='user2', + home='home3', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + daemon_conf = self.configuration.daemon_conf + daemon_conf['login_map']['user2'] = [cred] + result = login_map_lookup(daemon_conf, 'user2') + self.assertEqual(result, [cred]) - # No changes should be reported because the home directory is missing - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - - def test_refresh_user_creds_invalid_protocol(self): - """Test that an invalid protocol returns early without changes.""" - username = TEST_USER_EMAIL + def test_user_does_not_exist(self): + """Verify login_map_lookup returns empty list for non-existent user.""" + daemon_conf = self.configuration.daemon_conf + result = login_map_lookup(daemon_conf, 'nosuchuser') + self.assertEqual(result, []) - # Call the function under test with an invalid protocol - (updated_conf, changed_users) = refresh_user_creds( + def test_lock_handling(self): + """Basic check that creds_lock is acquired and released.""" + # Simulate concurrent access (not fully testable in unit tests) + # This is a placeholder for thread-safety verification + daemon_conf = self.configuration.daemon_conf + cred = Login( configuration=self.configuration, - protocol='invalid', - username=username + username='user3', + home='home4', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - - # No changes should be reported - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - -# TODO: merge X509 and UUID versions? + daemon_conf['login_map']['user3'] = [cred] + # Call the function (lock should be acquired/released internally) + result = login_map_lookup(daemon_conf, 'user3') + self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) -class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id(MigTestCase, UserAssertMixin): - """Unit tests for the griddaemons login refresh_user_creds helper with UUID users.""" +class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): + """Unit tests for the griddaemons login refresh_share_creds helper.""" def _provide_configuration(self): """Return a test configuration instance.""" @@ -1119,601 +1073,514 @@ def _provide_configuration(self): def before_each(self): """Set up test configuration and reset state before each test.""" - # Force UUID user id format - self.configuration.site_user_id_format = UUID_USER_ID_FORMAT - # Ensure required directories exist - ensure_dirs_exist(self.configuration.mig_system_files) - _ensure_dirs_needed_for_userdb(self.configuration) - self.expected_user_db_home = os.path.normpath( - self.configuration.user_db_home - ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" - ) - # NOTE: we need to set a password_hash for https test to work with - overrides = {} - overrides["password_hash"] = TEST_USER_PW_HASH - - user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN, - overrides) - self.test_user_id = user_id = user_dict['unique_id'] - client_dir = client_id_dir(TEST_USER_DN) - self.test_user_home = os.path.join(self.configuration.user_home, - user_id) - self.test_user_dir = os.path.basename(self.test_user_home) - self.test_user_home_x509 = os.path.join(self.configuration.user_home, - client_dir) - self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) - # Make sure X509 and alias links are provisioned as well - if not os.path.islink(self.test_user_home_x509): - os.symlink(self.test_user_home, self.test_user_home_x509) - alias_link_path = os.path.join(self.configuration.user_home, - TEST_USER_EMAIL) - if not os.path.islink(alias_link_path): - os.symlink(self.test_user_home_x509, alias_link_path) - - ALIAS_FIELD = 'email' - self.configuration.user_sftp_alias = ALIAS_FIELD - self.configuration.user_ftps_alias = ALIAS_FIELD - self.configuration.user_davs_alias = ALIAS_FIELD + # The base class already creates the required directory layout. + # Ensure the share‑link home exists – it is used by refresh_share_creds. + self.ro_share_home = os.path.join(self.configuration.sharelink_home, + 'read-only') + self.rw_share_home = os.path.join(self.configuration.sharelink_home, + 'read-write') + self.wo_share_home = os.path.join(self.configuration.sharelink_home, + 'write-only') + ensure_dirs_exist(self.ro_share_home) + ensure_dirs_exist(self.rw_share_home) + ensure_dirs_exist(self.wo_share_home) - # Common daemon configuration self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['shares'] = [] self.configuration.daemon_conf['allow_publickey'] = True self.configuration.daemon_conf['allow_password'] = True self.configuration.daemon_conf['allow_digest'] = False - self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD - def test_refresh_user_creds_ssh_protocol(self): - """Test refreshing user credentials for SSH protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + # Paths that the function will look at + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) + self.auth_digests_path = None - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) + # Create dummy credential files + with open(self.auth_keys_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PUB_KEY) + with open(self.auth_passwords_path, 'w') as creds_fd: + creds_fd.write(TEST_USER_PW_HASH) - # The user should be in the changed list - self.assertIn(username, changed_users) + def test_refresh_share_creds_adds_new_share(self): + """A new share link should be added to daemon_conf['shares'].""" + # Build a share link that points to a temporary user directory + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least one login (the main username) and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) - - # Check that at least one login has the correct home directory - # TODO: is this X509 dir what we want here or the UUID one? - home_found = any(login.home == self.test_user_dir_x509 for login in - user_logins) - self.assertTrue(home_found) - - def test_refresh_user_creds_davs_protocol(self): - """Test refreshing user credentials for DAVS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['davs']) + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + # NOTE: only sftp access is supported for now + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol='davs', - username=username + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) ) - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - # TODO: is this X509 dir what we want here or the UUID one? - self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + # The share should now be present in the changed list + self.assertIn(TEST_RW_SHARE_ID, changed_shares) - def test_refresh_user_creds_ftps_protocol(self): - """Test refreshing user credentials for FTPS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ftps']) + # Verify that a Login object was added to shares + share_login = [ + obj for obj in updated_conf['shares'] + if obj.username == TEST_RW_SHARE_ID + ] + self.assertEqual(len(share_login), 1) - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ftps', - username=username - ) + # The added object should contain the expected home directory + self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) + self.assertEqual(share_login[0].home, rel_share_home) + # TODO: check password hash, too? + # share_pw_hash = generate_password_hash(self.configuration, + # TEST_RW_SHARE_ID) + # self.assertEqual(share_login[0].password, share_pw_hash) - # The user should be in the changed list - self.assertIn(username, changed_users) + def test_refresh_share_creds_adds_new_share_with_key(self): + """A new share link with key should be added twice to daemon_conf['shares'].""" + # Build a share link that points to a temporary user directory + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - # TODO: is this X509 dir what we want here or the UUID one? - self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) - def test_refresh_user_creds_https_protocol(self): - """Test refreshing user credentials for HTTPS protocol (uses user DB).""" - username = TEST_USER_EMAIL + _prepare_auth_files(user_shared_dir, ['ssh']) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + # NOTE: only sftp access is supported for now + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol='https', - username=username + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) ) - # The user alias should be in the changed list - self.assertIn(TEST_USER_EMAIL, changed_users) + # The share should now be present twice in the changed list + self.assertIn(TEST_RW_SHARE_ID, changed_shares) - # Verify that Login objects were added to users for the username and its aliases - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least the main username and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) + # Verify that a Login object was added to shares + share_login = [ + obj for obj in updated_conf['shares'] + if obj.username == TEST_RW_SHARE_ID + ] + self.assertEqual(len(share_login), 2) - # Check that at least one login has the correct home directory - # TODO: is this X509 dir what we want here or the UUID one? - home_found = any(login.home == self.test_user_dir_x509 for login in - user_logins) - self.assertTrue(home_found) + # The added objects should contain the expected home directory + self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) + self.assertEqual(share_login[0].home, rel_share_home) + self.assertEqual(share_login[1].username, TEST_RW_SHARE_ID) + self.assertEqual(share_login[1].home, rel_share_home) + # TODO: check password hash, too? + # share_pw_hash = generate_password_hash(self.configuration, + # TEST_RW_SHARE_ID) + # self.assertEqual(share_login[0].password, share_pw_hash) + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = share_login[1].public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) - def test_refresh_user_creds_no_changes(self): - """Test that no changes are reported when credentials are unchanged.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + def test_refresh_share_creds_no_changes(self): + """When the share link and its key file have not changed, the function + should return an empty changed_shares list.""" + daemon_conf = self.configuration.daemon_conf - # Pre-populate the users list with a Login object that has - # last_update set to the current time (simulating no changes) + # Create a share link that already exists + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + # Populate shares with a dummy entry whose last_update matches + # the current file mtime – this simulates “no changes”. current_time = time.time() - dummy_user = Login( + dummy_share = Login( configuration=self.configuration, - username=username, - home=self.test_user_home, - password=TEST_USER_PW_HASH, + username=TEST_RW_SHARE_ID, + home=rel_share_home, + password=TEST_RW_SHARE_ID, digest=None, - public_key=TEST_USER_PUB_KEY, + public_key=None, chroot=True, access=None, ip_addr=None, user_dict=None) - dummy_user.last_update = current_time - self.configuration.daemon_conf['users'].append(dummy_user) + dummy_share.last_update = current_time + daemon_conf['shares'].append(dummy_share) - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + # Run the function + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol='ssh', - username=username + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) ) # No changes should be reported - self.assertEqual(len(changed_users), 0) - # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf['users']), 1) - self.assertEqual(updated_conf['users'][0].username, username) + self.assertEqual(len(changed_shares), 0) - def test_refresh_user_creds_missing_user(self): - """Test that a missing user is skipped.""" - username = 'nosuchuser' + # The dummy entry should still be the only one present + self.assertEqual(len(updated_conf['shares']), 1) - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + def test_refresh_share_creds_adds_readonly_share(self): + """Test that a read‑only share is added correctly""" + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) + + share_link_path = os.path.join(self.configuration.sharelink_home, + 'read-only', TEST_RO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol='ssh', - username=username + protocol='sftp', + username=TEST_RO_SHARE_ID, + share_modes=(READ_ONLY_ACCESS, ) ) - # No changes should be reported because the home directory is missing - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) + self.assertIn(TEST_RO_SHARE_ID, changed_shares) + share_login = [obj for obj in updated_conf['shares'] + if obj.username == TEST_RO_SHARE_ID] + self.assertEqual(len(share_login), 1) + self.assertEqual(share_login[0].home, rel_share_home) - def test_refresh_user_creds_invalid_protocol(self): - """Test that an invalid protocol returns early without changes.""" - username = TEST_USER_EMAIL + def test_refresh_share_creds_adds_writeonly_share(self): + """Test that a write‑only share is added correctly""" + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) + ensure_dirs_exist(user_shared_dir) - # Call the function under test with an invalid protocol - (updated_conf, changed_users) = refresh_user_creds( + share_link_path = os.path.join(self.wo_share_home, TEST_WO_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol='invalid', - username=username + protocol='sftp', + username=TEST_WO_SHARE_ID, + share_modes=(WRITE_ONLY_ACCESS, ) ) - # No changes should be reported - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - - -class MigSharedGriddaemonsLogin__login_map_lookup(MigTestCase): - """Unit tests for the login_map_lookup function from griddaemons/login.py""" + self.assertIn(TEST_WO_SHARE_ID, changed_shares) + share_login = [obj for obj in updated_conf['shares'] + if obj.username == TEST_WO_SHARE_ID] + self.assertEqual(len(share_login), 1) + self.assertEqual(share_login[0].home, rel_share_home) - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' + def test_refresh_share_creds_no_change_on_unchanged_link(self): + """Test that an unchanged share link does not trigger a change""" + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home) + ensure_dirs_exist(user_shared_dir) - def before_each(self): - """Set up test configuration with login_map and creds_lock.""" - # Common daemon configuration - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['login_map'] = {} - self.configuration.daemon_conf['creds_lock'] = threading.Lock() + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(user_shared_dir, share_link_path) - def test_user_exists_with_multiple_credentials(self): - """Verify login_map_lookup returns all credentials for a user.""" - # Create two Login objects for 'user1' - cred1 = Login( - configuration=self.configuration.daemon_conf, - username='user1', - home='home1', - password=None, + # Populate shares with a dummy entry whose last_update matches + # the current file mtime – this simulates “no changes”. + current_time = time.time() + dummy_share = Login( + configuration=self.configuration, + username=TEST_RW_SHARE_ID, + home=rel_share_home, + password=TEST_RW_SHARE_ID, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None) + dummy_share.last_update = current_time + self.configuration.daemon_conf['shares'].append(dummy_share) + + (updated_conf, changed_shares) = refresh_share_creds( + configuration=self.configuration, + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) ) - cred2 = Login( + + self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(updated_conf['shares']), 1) + + def test_refresh_share_creds_detects_missing_link(self): + """Test that a missing share link is reported as a change""" + # No symlink created - share link is missing + missing_share_id = 'missing123' + + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - username='user1', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='sftp', + username=missing_share_id, + share_modes=(READ_WRITE_ACCESS, ) ) - # Populate login_map - daemon_conf = self.configuration.daemon_conf - daemon_conf['login_map']['user1'] = [cred1, cred2] - # Call the function - result = login_map_lookup(daemon_conf, 'user1') - # Assert - self.assertEqual(result, [cred1, cred2]) - def test_user_exists_with_single_credential(self): - """Verify login_map_lookup returns one credential for a user.""" - cred = Login( + # The function should still return an empty list because the link + # does not exist; no share is added. + self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(updated_conf['shares']), 0) + + def test_refresh_share_creds_ignores_dead_link(self): + """Test that a dead share link is ignored""" + # Create a symlink that points nowhere + invalid_target = os.path.join(self.configuration.user_home, 'deadbeef') + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(invalid_target, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - username='user2', - home='home3', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) ) - daemon_conf = self.configuration.daemon_conf - daemon_conf['login_map']['user2'] = [cred] - result = login_map_lookup(daemon_conf, 'user2') - self.assertEqual(result, [cred]) - def test_user_does_not_exist(self): - """Verify login_map_lookup returns empty list for non-existent user.""" - daemon_conf = self.configuration.daemon_conf - result = login_map_lookup(daemon_conf, 'nosuchuser') - self.assertEqual(result, []) + # No share should be added because the link is invalid + self.assertEqual(len(changed_shares), 0) + self.assertEqual(len(updated_conf['shares']), 0) - def test_lock_handling(self): - """Basic check that creds_lock is acquired and released.""" - # Simulate concurrent access (not fully testable in unit tests) - # This is a placeholder for thread-safety verification - daemon_conf = self.configuration.daemon_conf - cred = Login( + def test_refresh_share_creds_ignores_invalid_link(self): + """Test that an invalid (out of bounds) share link is ignored""" + # Create a symlink that points outside a user_home directory + invalid_target = self.configuration.certs_path + ensure_dirs_exist(invalid_target) + share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) + os.symlink(invalid_target, share_link_path) + + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - username='user3', - home='home4', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='sftp', + username=TEST_RW_SHARE_ID, + share_modes=(READ_WRITE_ACCESS, ) ) - daemon_conf['login_map']['user3'] = [cred] - # Call the function (lock should be acquired/released internally) - result = login_map_lookup(daemon_conf, 'user3') - self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) + # No share should be added because the link is invalid but change + # is still reported as modified. + self.assertEqual(len(changed_shares), 1) + self.assertEqual(changed_shares[0], TEST_RW_SHARE_ID) + self.assertEqual(len(updated_conf['shares']), 0) -class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): - """Unit tests for the update_login_map function from griddaemons/login.py""" + +class MigSharedGriddaemonsLogin__refresh_user_creds(MigTestCase, UserAssertMixin): + """Unit tests for the griddaemons login refresh_user_creds helper.""" def _provide_configuration(self): """Return a test configuration instance.""" return 'testconfig' def before_each(self): - """Set up test configuration with login_map and creds_lock.""" + """Set up test configuration and reset state before each test.""" + # Force X509 user id format + self.configuration.site_user_id_format = X509_USER_ID_FORMAT + # Ensure required directories exist + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + self.configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + self.test_user_home = self._provision_test_user(self, TEST_USER_DN) + self.test_user_dir = os.path.basename(self.test_user_home) + # Make sure alias link is provisioned as well + alias_link_path = os.path.join(self.configuration.user_home, + TEST_USER_EMAIL) + if not os.path.islink(alias_link_path): + os.symlink(self.test_user_home, alias_link_path) + + ALIAS_FIELD = 'email' + self.configuration.user_sftp_alias = ALIAS_FIELD + self.configuration.user_ftps_alias = ALIAS_FIELD + self.configuration.user_davs_alias = ALIAS_FIELD + # Common daemon configuration self.configuration.daemon_conf = {} self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['login_map'] = {} self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['jobs'] = [] - self.configuration.daemon_conf['shares'] = [] - self.configuration.daemon_conf['jupyter_mounts'] = [] - self.configuration.daemon_conf['creds_lock'] = threading.Lock() + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD - def test_update_login_map_users(self): - """Verify login_map is updated correctly for changed users.""" - # Create Login objects - user1 = Login( + def test_refresh_user_creds_ssh_protocol(self): + """Test refreshing user credentials for SSH protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='ssh', + username=username ) - user2 = Login( + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least one login (the main username) and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + home_found = any(login.home == self.test_user_dir for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_davs_protocol(self): + """Test refreshing user credentials for DAVS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['davs']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - username='user2', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='davs', + username=username ) - # Populate users list - self.configuration.daemon_conf['users'] = [user1, user2] + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + self.assertEqual(user_logins[0].home, self.test_user_dir) + + def test_refresh_user_creds_ftps_protocol(self): + """Test refreshing user credentials for FTPS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ftps']) # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['user1', 'user2'] + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ftps', + username=username ) - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('user1', login_map) - self.assertIn('user2', login_map) - self.assertEqual(login_map['user1'], [user1]) - self.assertEqual(login_map['user2'], [user2]) + # The user should be in the changed list + self.assertIn(username, changed_users) - def test_update_login_map_jobs(self): - """Verify login_map is updated correctly for changed jobs.""" - # Create Login objects - job1 = Login( - configuration=self.configuration, - username='job1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr='1.2.3.4', - user_dict=None - ) - job2 = Login( - configuration=self.configuration, - username='job2', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr='5.6.7.8', - user_dict=None - ) - # Populate jobs list - self.configuration.daemon_conf['jobs'] = [job1, job2] + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + self.assertEqual(user_logins[0].home, self.test_user_dir) + + def test_refresh_user_creds_https_protocol(self): + """Test refreshing user credentials for HTTPS protocol (uses user DB).""" + username = TEST_USER_EMAIL # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_jobs=['job1', 'job2'] + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='https', + username=username ) - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('job1', login_map) - self.assertIn('job2', login_map) - self.assertEqual(login_map['job1'], [job1]) - self.assertEqual(login_map['job2'], [job2]) + # The user alias should be in the changed list + self.assertIn(TEST_USER_EMAIL, changed_users) - def test_update_login_map_shares(self): - """Verify login_map is updated correctly for changed shares.""" - # Create Login objects - share1 = Login( - configuration=self.configuration, - username='share1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - share2 = Login( - configuration=self.configuration, - username='share2', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - # Populate shares list - self.configuration.daemon_conf['shares'] = [share1, share2] + # Verify that Login objects were added to users for the username and its aliases + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least the main username and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_shares=['share1', 'share2'] - ) + # Check that at least one login has the correct home directory + home_found = any(login.home == self.test_user_dir for login in + user_logins) + self.assertTrue(home_found) - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('share1', login_map) - self.assertIn('share2', login_map) - self.assertEqual(login_map['share1'], [share1]) - self.assertEqual(login_map['share2'], [share2]) + def test_refresh_user_creds_no_changes(self): + """Test that no changes are reported when credentials are unchanged.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) - def test_update_login_map_jupyter(self): - """Verify login_map is updated correctly for changed jupyter mounts.""" - # Create Login objects - jupyter1 = Login( - configuration=self.configuration, - username='jupyter1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - jupyter2 = Login( + # Pre-populate the users list with a Login object that has + # last_update set to the current time (simulating no changes) + current_time = time.time() + dummy_user = Login( configuration=self.configuration, - username='jupyter2', - home='home2', - password=None, + username=username, + home=self.test_user_home, + password=TEST_USER_PW_HASH, digest=None, - public_key=None, + public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None - ) - # Populate jupyter_mounts list - self.configuration.daemon_conf['jupyter_mounts'] = [jupyter1, jupyter2] + user_dict=None) + dummy_user.last_update = current_time + self.configuration.daemon_conf['users'].append(dummy_user) # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_jupyter=['jupyter1', 'jupyter2'] + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username ) - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('jupyter1', login_map) - self.assertIn('jupyter2', login_map) - self.assertEqual(login_map['jupyter1'], [jupyter1]) - self.assertEqual(login_map['jupyter2'], [jupyter2]) + # No changes should be reported + self.assertEqual(len(changed_users), 0) + # The user list should still contain only our dummy user + self.assertEqual(len(updated_conf['users']), 1) + self.assertEqual(updated_conf['users'][0].username, username) - def test_update_login_map_nonexistent(self): - """Verify login_map is set to empty list for non-existent usernames.""" - # Populate users list with one user - user1 = Login( + def test_refresh_user_creds_missing_user(self): + """Test that a missing user is skipped.""" + username = 'nosuchuser' + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='ssh', + username=username ) - self.configuration.daemon_conf['users'] = [user1] - # Call the function under test for a non-existent user - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['nonexistent'] - ) + # No changes should be reported because the home directory is missing + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('nonexistent', login_map) - self.assertEqual(login_map['nonexistent'], []) + def test_refresh_user_creds_invalid_protocol(self): + """Test that an invalid protocol returns early without changes.""" + username = TEST_USER_EMAIL - def test_update_login_map_empty_lists(self): - """Verify login_map is not changed when changed lists are empty.""" - # Populate users list - user1 = Login( + # Call the function under test with an invalid protocol + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None + protocol='invalid', + username=username ) - self.configuration.daemon_conf['users'] = [user1] - # Call the function under test with empty lists - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_jobs=[], - changed_shares=[], - changed_jupyter=[] - ) + # No changes should be reported + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) - # Verify login_map is unchanged (should be empty because we didn't update for 'user1') - login_map = self.configuration.daemon_conf['login_map'] - self.assertEqual(len(login_map), 0) +# TODO: merge X509 and UUID versions? - def test_update_login_map_lock_handling(self): - """Basic check that creds_lock is acquired and released.""" - # Create a Login object - user1 = Login( - configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - self.configuration.daemon_conf['users'] = [user1] - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['user1'] - ) - - # Verify login_map was updated (lock must have been acquired/released) - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('user1', login_map) - self.assertEqual(login_map['user1'], [user1]) - - -class MigSharedGriddaemonsLogin__add_job_object(MigTestCase): - """Unit tests for the griddaemons add_job_object helper.""" +class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id(MigTestCase, UserAssertMixin): + """Unit tests for the griddaemons login refresh_user_creds helper with UUID users.""" def _provide_configuration(self): """Return a test configuration instance.""" @@ -1721,384 +1588,499 @@ def _provide_configuration(self): def before_each(self): """Set up test configuration and reset state before each test.""" - # Initialize daemon_conf with empty jobs list - self.configuration.daemon_conf = { - 'jobs': [], - 'creds_lock': threading.Lock() # Ensure lock is present for testing - } - - def test_add_job_object_basic(self): - """Verify that add_job_object adds a Login object to the jobs list.""" - # Call the helper - add_job_object( - configuration=self.configuration, - login='job1', - home='home1', - password=None, - pubkey=TEST_USER_PUB_KEY + # Force UUID user id format + self.configuration.site_user_id_format = UUID_USER_ID_FORMAT + # Ensure required directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + self.configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" ) + # NOTE: we need to set a password_hash for https test to work with + overrides = {} + overrides["password_hash"] = TEST_USER_PW_HASH - # Verify the job was added - jobs = self.configuration.daemon_conf['jobs'] - self.assertEqual(len(jobs), 1) - job = jobs[0] - self.assertEqual(job.username, 'job1') - self.assertEqual(job.home, 'home1') - self.assertEqual(job.password, None) - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = job.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN, + overrides) + self.test_user_id = user_id = user_dict['unique_id'] + client_dir = client_id_dir(TEST_USER_DN) + self.test_user_home = os.path.join(self.configuration.user_home, + user_id) + self.test_user_dir = os.path.basename(self.test_user_home) + self.test_user_home_x509 = os.path.join(self.configuration.user_home, + client_dir) + self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) + # Make sure X509 and alias links are provisioned as well + if not os.path.islink(self.test_user_home_x509): + os.symlink(self.test_user_home, self.test_user_home_x509) + alias_link_path = os.path.join(self.configuration.user_home, + TEST_USER_EMAIL) + if not os.path.islink(alias_link_path): + os.symlink(self.test_user_home_x509, alias_link_path) - def test_add_job_object_with_lock(self): - """Verify that add_job_object acquires and releases the creds_lock.""" - # Call the helper - add_job_object( - configuration=self.configuration, - login='job2', - home='home2' - ) + ALIAS_FIELD = 'email' + self.configuration.user_sftp_alias = ALIAS_FIELD + self.configuration.user_ftps_alias = ALIAS_FIELD + self.configuration.user_davs_alias = ALIAS_FIELD + + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD - # Check lock state (simplified verification) - lock = self.configuration.daemon_conf['creds_lock'] - # Note: Direct lock state inspection is not ideal in unit tests - # This test assumes the lock is properly handled by the function + def test_refresh_user_creds_ssh_protocol(self): + """Test refreshing user credentials for SSH protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) - def test_add_job_object_missing_home(self): - """Verify that add_job_object uses login as home if home is None.""" - add_job_object( + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - login='job3', - home=None + protocol='ssh', + username=username ) - job = self.configuration.daemon_conf['jobs'][0] - self.assertEqual(job.home, 'job3') + # The user should be in the changed list + self.assertIn(username, changed_users) -class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): - """Unit tests for the griddaemons add_jupyter_object helper.""" + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least one login (the main username) and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' + # Check that at least one login has the correct home directory + # TODO: is this X509 dir what we want here or the UUID one? + home_found = any(login.home == self.test_user_dir_x509 for login in + user_logins) + self.assertTrue(home_found) - def before_each(self): - """Set up test configuration and reset state before each test.""" - # Initialize daemon_conf with empty jupyter_mounts list - self.configuration.daemon_conf = { - 'jupyter_mounts': [], - 'creds_lock': threading.Lock() - } + def test_refresh_user_creds_davs_protocol(self): + """Test refreshing user credentials for DAVS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['davs']) - def test_add_jupyter_object_basic(self): - """Verify that add_jupyter_object adds a Login object to jupyter_mounts.""" - # Call the helper - add_jupyter_object( + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - login='jupyter1', - home='jupyter_home1', - pubkey=TEST_USER_PUB_KEY + protocol='davs', + username=username ) - # Verify the jupyter mount was added - mounts = self.configuration.daemon_conf['jupyter_mounts'] - self.assertEqual(len(mounts), 1) - mount = mounts[0] - self.assertEqual(mount.username, 'jupyter1') - self.assertEqual(mount.home, 'jupyter_home1') - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = mount.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) + # The user should be in the changed list + self.assertIn(username, changed_users) - def test_add_jupyter_object_with_lock(self): - """Verify that add_jupyter_object handles the creds_lock.""" - # Call the helper - add_jupyter_object( + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + # TODO: is this X509 dir what we want here or the UUID one? + self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + + def test_refresh_user_creds_ftps_protocol(self): + """Test refreshing user credentials for FTPS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ftps']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - login='jupyter2', - home='jupyter_home2' + protocol='ftps', + username=username ) - # Check lock state (simplified verification) - lock = self.configuration.daemon_conf['creds_lock'] - # Assumes the function properly acquires/releases the lock + # The user should be in the changed list + self.assertIn(username, changed_users) + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + # TODO: is this X509 dir what we want here or the UUID one? + self.assertEqual(user_logins[0].home, self.test_user_dir_x509) -class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): - """Unit tests for the add_user_object helper.""" + def test_refresh_user_creds_https_protocol(self): + """Test refreshing user credentials for HTTPS protocol (uses user DB).""" + username = TEST_USER_EMAIL - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='https', + username=username + ) - def before_each(self): - """Set up daemon_conf for add_user_object tests.""" - self.configuration.daemon_conf = { - 'users': [], - 'creds_lock': threading.Lock() - } + # The user alias should be in the changed list + self.assertIn(TEST_USER_EMAIL, changed_users) - def test_add_user_object_appends_login(self): - """Verify add_user_object appends a Login to users.""" - user_dict = {'user_id': TEST_USER_DN} + # Verify that Login objects were added to users for the username and its aliases + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least the main username and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) - add_user_object( + # Check that at least one login has the correct home directory + # TODO: is this X509 dir what we want here or the UUID one? + home_found = any(login.home == self.test_user_dir_x509 for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_no_changes(self): + """Test that no changes are reported when credentials are unchanged.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Pre-populate the users list with a Login object that has + # last_update set to the current time (simulating no changes) + current_time = time.time() + dummy_user = Login( configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1', + username=username, + home=self.test_user_home, password=TEST_USER_PW_HASH, - access=READ_WRITE_ACCESS, - user_dict=user_dict - ) - - self.assertEqual(len(self.configuration.daemon_conf['users']), 1) - login = self.configuration.daemon_conf['users'][0] - self.assertIsInstance(login, Login) - self.assertEqual(login.username, TEST_USER_EMAIL) - self.assertEqual(login.home, 'home1') - self.assertEqual(login.password, TEST_USER_PW_HASH) - self.assertIsNone(login.digest) - self.assertIsNone(login.public_key) - self.assertTrue(login.chroot) - self.assertEqual(login.access, READ_WRITE_ACCESS) - self.assertEqual(login.user_dict, user_dict) + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None) + dummy_user.last_update = current_time + self.configuration.daemon_conf['users'].append(dummy_user) - def test_add_user_object_adds_public_key_login(self): - """Verify add_user_object stores parsed public key logins.""" - add_user_object( + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1', - pubkey=TEST_USER_PUB_KEY, - access=READ_WRITE_ACCESS + protocol='ssh', + username=username ) - login = self.configuration.daemon_conf['users'][0] - self.assertIsNotNone(login.public_key) - key_line = "%s %s" % (login.public_key.get_name(), - login.public_key.get_base64()) - self.assertEqual(key_line, TEST_USER_PUB_KEY) + # No changes should be reported + self.assertEqual(len(changed_users), 0) + # The user list should still contain only our dummy user + self.assertEqual(len(updated_conf['users']), 1) + self.assertEqual(updated_conf['users'][0].username, username) - def test_add_user_object_adds_digest_login(self): - """Verify add_user_object stores digest logins.""" - add_user_object( + def test_refresh_user_creds_missing_user(self): + """Test that a missing user is skipped.""" + username = 'nosuchuser' + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1', - digest='digest-value', - access=READ_ONLY_ACCESS + protocol='ssh', + username=username ) - login = self.configuration.daemon_conf['users'][0] - self.assertEqual(login.digest, 'digest-value') - self.assertIsNone(login.password) - self.assertIsNone(login.public_key) - self.assertEqual(login.access, READ_ONLY_ACCESS) + # No changes should be reported because the home directory is missing + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) - def test_add_user_object_without_lock(self): - """Verify add_user_object works when creds_lock is not configured.""" - daemon_conf = self.configuration.daemon_conf - daemon_conf.pop('creds_lock') + def test_refresh_user_creds_invalid_protocol(self): + """Test that an invalid protocol returns early without changes.""" + username = TEST_USER_EMAIL - add_user_object( + # Call the function under test with an invalid protocol + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1' + protocol='invalid', + username=username ) - self.assertEqual(len(daemon_conf['users']), 1) - self.assertEqual(daemon_conf['users'][0].username, TEST_USER_EMAIL) - - def test_add_user_object_uses_creds_lock(self): - """Verify add_user_object acquires and releases creds_lock.""" - class FakeLock(object): - """Small lock double for add_user_object tests.""" + # No changes should be reported + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) - def __init__(self): - self.acquired = False - self.released = False - def acquire(self): - self.acquired = True +class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): + """Unit tests for the update_login_map function from griddaemons/login.py""" - def release(self): - self.released = True + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' - fake_lock = FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock + def before_each(self): + """Set up test configuration with login_map and creds_lock.""" + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['login_map'] = {} + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['jobs'] = [] + self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['jupyter_mounts'] = [] + self.configuration.daemon_conf['creds_lock'] = threading.Lock() - add_user_object( + def test_update_login_map_users(self): + """Verify login_map is updated correctly for changed users.""" + # Create Login objects + user1 = Login( configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1' + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - - self.assertTrue(fake_lock.acquired) - self.assertTrue(fake_lock.released) - - def test_add_user_object_uses_login_as_home_when_home_none(self): - """Verify add_user_object normalizes missing home to login name.""" - add_user_object( + user2 = Login( configuration=self.configuration, - login=TEST_USER_EMAIL, - home=None + username='user2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) + # Populate users list + self.configuration.daemon_conf['users'] = [user1, user2] - login = self.configuration.daemon_conf['users'][0] - self.assertEqual(login.home, TEST_USER_EMAIL) + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=['user1', 'user2'] + ) + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('user1', login_map) + self.assertIn('user2', login_map) + self.assertEqual(login_map['user1'], [user1]) + self.assertEqual(login_map['user2'], [user2]) -class MigSharedGriddaemonsLogin__add_share_object(MigTestCase): - """Unit tests for the add_share_object helper.""" + def test_update_login_map_jobs(self): + """Verify login_map is updated correctly for changed jobs.""" + # Create Login objects + job1 = Login( + configuration=self.configuration, + username='job1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr='1.2.3.4', + user_dict=None + ) + job2 = Login( + configuration=self.configuration, + username='job2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr='5.6.7.8', + user_dict=None + ) + # Populate jobs list + self.configuration.daemon_conf['jobs'] = [job1, job2] - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_jobs=['job1', 'job2'] + ) - def before_each(self): - """Set up daemon_conf for add_share_object tests.""" - self.configuration.daemon_conf = { - 'shares': [], - 'creds_lock': threading.Lock() - } + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('job1', login_map) + self.assertIn('job2', login_map) + self.assertEqual(login_map['job1'], [job1]) + self.assertEqual(login_map['job2'], [job2]) - def test_add_share_object_appends_login(self): - """Verify add_share_object appends a Login to shares.""" - user_dict = {} - home_path = temppath('share_home', self) - ensure_dirs_exist(home_path) - login = Login( + def test_update_login_map_shares(self): + """Verify login_map is updated correctly for changed shares.""" + # Create Login objects + share1 = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS, - user_dict=user_dict + username='share1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path, - password=login.password, - access=login.access - ) - self.assertEqual(len(self.configuration.daemon_conf['shares']), 1) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.username, TEST_RW_SHARE_ID) - self.assertEqual(share_login.home, home_path) - self.assertEqual(share_login.password, TEST_RW_SHARE_ID) - self.assertTrue(share_login.chroot) - self.assertEqual(share_login.access, READ_WRITE_ACCESS) - - def test_add_share_object_adds_public_key_login(self): - """Verify add_share_object stores parsed public key logins.""" - home_path = temppath('share_home_pubkey', self) - ensure_dirs_exist(home_path) - login = Login( + share2 = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + username='share2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - add_share_object( - configuration=self.configuration, - login=login, - home=home_path, - pubkey=TEST_USER_PUB_KEY + # Populate shares list + self.configuration.daemon_conf['shares'] = [share1, share2] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_shares=['share1', 'share2'] ) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertIsNotNone(share_login.public_key) - key_line = "%s %s" % (share_login.public_key.get_name(), - share_login.public_key.get_base64()) - self.assertEqual(key_line, TEST_USER_PUB_KEY) - def test_add_share_object_adds_digest_login(self): - """Verify add_share_object stores digest logins.""" - home_path = temppath('share_home_digest', self) - ensure_dirs_exist(home_path) - login = Login( + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('share1', login_map) + self.assertIn('share2', login_map) + self.assertEqual(login_map['share1'], [share1]) + self.assertEqual(login_map['share2'], [share2]) + + def test_update_login_map_jupyter(self): + """Verify login_map is updated correctly for changed jupyter mounts.""" + # Create Login objects + jupyter1 = Login( configuration=self.configuration, - username=TEST_RO_SHARE_ID, - home=home_path, - password=TEST_RO_SHARE_ID, - access=READ_ONLY_ACCESS + username='jupyter1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - add_share_object( + jupyter2 = Login( configuration=self.configuration, - login=login, - home=home_path, - digest='digest-value', - access=READ_ONLY_ACCESS + username='jupyter2', + home='home2', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.digest, 'digest-value') - self.assertIsNone(share_login.password) - self.assertIsNone(share_login.public_key) - self.assertEqual(share_login.access, READ_ONLY_ACCESS) + # Populate jupyter_mounts list + self.configuration.daemon_conf['jupyter_mounts'] = [jupyter1, jupyter2] - def test_add_share_object_without_lock(self): - """Verify add_share_object works when creds_lock is not configured.""" - daemon_conf = self.configuration.daemon_conf - daemon_conf.pop('creds_lock') - home_path = temppath('share_home_nolock', self) - ensure_dirs_exist(home_path) - login = Login( + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_jupyter=['jupyter1', 'jupyter2'] + ) + + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('jupyter1', login_map) + self.assertIn('jupyter2', login_map) + self.assertEqual(login_map['jupyter1'], [jupyter1]) + self.assertEqual(login_map['jupyter2'], [jupyter2]) + + def test_update_login_map_nonexistent(self): + """Verify login_map is set to empty list for non-existent usernames.""" + # Populate users list with one user + user1 = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path) - self.assertEqual(len(daemon_conf['shares']), 1) - self.assertEqual(daemon_conf['shares'][0].username, TEST_RW_SHARE_ID) + self.configuration.daemon_conf['users'] = [user1] - def test_add_share_object_uses_creds_lock(self): - """Verify add_share_object acquires and releases creds_lock.""" - class FakeLock(object): - def __init__(self): - self.acquired = False - self.released = False + # Call the function under test for a non-existent user + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=['nonexistent'] + ) - def acquire(self): - self.acquired = True + # Verify login_map + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('nonexistent', login_map) + self.assertEqual(login_map['nonexistent'], []) - def release(self): - self.released = True - fake_lock = FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - home_path = temppath('share_home_lock', self) - ensure_dirs_exist(home_path) - login = Login( + def test_update_login_map_empty_lists(self): + """Verify login_map is not changed when changed lists are empty.""" + # Populate users list + user1 = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path) - self.assertTrue(fake_lock.acquired) - self.assertTrue(fake_lock.released) + self.configuration.daemon_conf['users'] = [user1] - def test_add_share_object_uses_login_as_home_when_home_none(self): - """Verify add_share_object normalizes missing home to login name.""" - login = Login( + # Call the function under test with empty lists + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=[], + changed_jobs=[], + changed_shares=[], + changed_jupyter=[] + ) + + # Verify login_map is unchanged (should be empty because we didn't update for 'user1') + login_map = self.configuration.daemon_conf['login_map'] + self.assertEqual(len(login_map), 0) + + def test_update_login_map_lock_handling(self): + """Basic check that creds_lock is acquired and released.""" + # Create a Login object + user1 = Login( configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=None, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + username='user1', + home='home1', + password=None, + digest=None, + public_key=None, + chroot=True, + access=None, + ip_addr=None, + user_dict=None ) - add_share_object(configuration=self.configuration, - login=login.username, - home=None) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.home, TEST_RW_SHARE_ID) + self.configuration.daemon_conf['users'] = [user1] + + # Call the function under test + update_login_map( + daemon_conf=self.configuration.daemon_conf, + changed_users=['user1'] + ) + + # Verify login_map was updated (lock must have been acquired/released) + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('user1', login_map) + self.assertEqual(login_map['user1'], [user1]) class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): From c2f848e92f4a6020027a3f945193bc426a413116 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 15:28:03 +0200 Subject: [PATCH 35/61] Comment-only changes. --- mig/shared/griddaemons/login.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/mig/shared/griddaemons/login.py b/mig/shared/griddaemons/login.py index 83218300c..0d472f3d8 100644 --- a/mig/shared/griddaemons/login.py +++ b/mig/shared/griddaemons/login.py @@ -635,8 +635,7 @@ def refresh_job_creds(configuration, protocol, username): sessionid = username job_dict = unpickle(link_path, logger) - # We only allow connections from executing jobs that - # has a public key + # We only allow connections from executing jobs with a public key if job_dict is not None and isinstance(job_dict, dict) and \ 'STATUS' in job_dict and \ job_dict['STATUS'] == 'EXECUTING' and \ @@ -851,15 +850,15 @@ def refresh_jupyter_creds(configuration, protocol, username): logger.error("invalid protocol: %s" % protocol) return (conf, active_jupyter_creds) if not possible_jupyter_mount_id(configuration, username): - # logger.debug("ruled out %s as a possible jupyter_mount ID" \ - # % username) + # logger.debug("ruled out %s as a possible jupyter_mount ID" % + # username) return (conf, active_jupyter_creds) logger.info("Getting active jupyter mount creds") link_path = os.path.join(configuration.sessid_to_jupyter_mount_link_home, "%s" % username + ".jupyter_mount") - # logger.debug("jupyter linkpath: %s jupyter path exists: %s" % \ - # (os.path.islink(link_path), os.path.exists(link_path))) + # logger.debug("jupyter linkpath: %s jupyter path exists: %s" % + # (os.path.islink(link_path), os.path.exists(link_path))) jupyter_dict, sessionid = None, None if os.path.islink(link_path) and os.path.exists(link_path): From 1b05d1acaf75ebf931dce6cab5982397518fbbd5 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 15:30:52 +0200 Subject: [PATCH 36/61] Complete fundamental unit test coverage of griddaemons.login module. --- tests/test_mig_shared_griddaemons.py | 453 +++++++++++++++++++++++++-- 1 file changed, 426 insertions(+), 27 deletions(-) diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py index f4a515639..09d6b9952 100644 --- a/tests/test_mig_shared_griddaemons.py +++ b/tests/test_mig_shared_griddaemons.py @@ -29,6 +29,8 @@ import binascii import os +import pickle +import socket import threading import time import unittest @@ -48,7 +50,8 @@ from mig.shared.griddaemons.login import ( Login, add_job_object, add_jupyter_object, add_share_object, add_user_object, get_creds_changes, get_job_changes, get_share_changes, - login_map_lookup, refresh_share_creds, refresh_user_creds, + login_map_lookup, refresh_job_creds, refresh_jupyter_creds, + refresh_share_creds, refresh_user_creds, update_login_map, update_user_objects ) @@ -67,6 +70,18 @@ TEST_RW_SHARE_ID = 'klmnop4567' TEST_WO_SHARE_ID = 'uvwxyz7890' +# NOTE: job IDs and jupyter sessions are 64 chars with jobs limited to hex +TEST_JOB_ID = \ + "0419b45ebc1dedbdcb91fa6251035a2096758f5d700e15478b27a90734454107" +TEST_JOB_USER_DN = TEST_USER_DN +TEST_JOB_USER_DIR = client_id_dir(TEST_USER_DN) +OTHER_JOB_ID = \ + "162b1575f7f62baf5c830cc7956a438f7d003b11cddbf6d21cfdc4f598fbf7c1" +TEST_JUPYTER_SESSION_ID = \ + "ohNo4ii9geeyei3Jai8aif6gae6Eebiechai3chegh0moo9NieveKu3AC8ooshuo" +OTHER_JUPYTER_SESSION_ID = \ + "sah3quoh3zedaemoovoowahlumeitumohv4iekooPhek3Ieng6Apho4aw0aiPhef" + # NOTE: this is a sample valid but unused ssh public key as it must be parsable TEST_USER_PUB_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuJrICshi7S2KhV03qvgNVOx5ejmHsswdGbvR34wf+eN23Ghq6OZhwGye2S+J6LPVFI3p4SCqxX4URnUM8BRAsiuvbf/+GQfE2pAO0C+/g4V3hhYbYzIyrtPsP1Hl8GioxvZD5nDoLEA4TWokDC4D7SRfv+NEkFLplyVBwHtpUunXBS/zXYdQ4lgk7u8HBBCMqUGbZHfCc+6ibFVn/5WS6vVokL+fSWtxg9tUVWqsS/xtDGPH1wbJUf1Dm3D58KmdX8ca73tBoScwH8qUQwEcyM1JtWtbv1BAZFb+Qk6SEe4GPRsn3I4AAgC7xtU3HKQsiqe8Fzpick/uI5PU+vguitcV/9+AASnGVZJ9M+a63UvlFFloEYcI1LwdZ03JYPQXfXCzJSYiA+pTX4/cf10G4rlxsque4m4OcuCwLKvpTWA/Lla+UJqYhdQW+m7mSizPRoDPgh8mOta1PQob2sGSw8rhqLfApptAPZ0mkN0QY3Dv3i3ItgpYGcPNVXVdjmhU=' # NOTE: this is a sample valid but unused ssh password hash as it must be parsable @@ -133,6 +148,40 @@ def _parse_pkey_to_openssh_format(paramiko_pkey): return "%s %s" % (paramiko_pkey.get_name(), paramiko_pkey.get_base64()) +def _create_job_mrsl_file(test_case, session_id, job_dict): + """Helper to create a .mRSL pickle file and symlink.""" + mrsl_dir = test_case.configuration.sessid_to_mrsl_link_home + ensure_dirs_exist(mrsl_dir) + + # Create actual pickle file in a temp location + pickle_path = temppath('job_%s.pkl' % session_id, test_case) + with open(pickle_path, 'wb') as pkl_fd: + pickle.dump(job_dict, pkl_fd) + + # Create symlink + link_path = os.path.join(mrsl_dir, '%s.mRSL' % session_id) + if os.path.lexists(link_path): + os.remove(link_path) + os.symlink(pickle_path, link_path) + return link_path + + +def _create_jupyter_mount_file(test_case, session_id, jupyter_dict): + """Helper to create a .jupyter_mount pickle file and symlink.""" + mount_dir = test_case.configuration.sessid_to_jupyter_mount_link_home + ensure_dirs_exist(mount_dir) + + pickle_path = temppath('jupyter_%s.pkl' % session_id, test_case) + with open(pickle_path, 'wb') as pkl_fd: + pickle.dump(jupyter_dict, pkl_fd) + + link_path = os.path.join(mount_dir, '%s.jupyter_mount' % session_id) + if os.path.lexists(link_path): + os.remove(link_path) + os.symlink(pickle_path, link_path) + return link_path + + class _FakeLock(object): """Small lock double for various tests with optional locking.""" @@ -167,7 +216,7 @@ def test_add_job_object_basic(self): # Call the helper add_job_object( configuration=self.configuration, - login='job1', + login=TEST_JOB_ID, home='home1', password=None, pubkey=TEST_USER_PUB_KEY @@ -177,7 +226,7 @@ def test_add_job_object_basic(self): jobs = self.configuration.daemon_conf['jobs'] self.assertEqual(len(jobs), 1) job = jobs[0] - self.assertEqual(job.username, 'job1') + self.assertEqual(job.username, TEST_JOB_ID) self.assertEqual(job.home, 'home1') self.assertEqual(job.password, None) # Convert saved paramiko.PKey back to openssh pub key format and check @@ -190,7 +239,7 @@ def test_add_job_object_uses_creds_lock(self): fake_lock = _FakeLock() self.configuration.daemon_conf['creds_lock'] = fake_lock add_job_object(configuration=self.configuration, - login='job2', + login=OTHER_JOB_ID, home='job_home2') self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) @@ -226,7 +275,7 @@ def test_add_jupyter_object_basic(self): # Call the helper add_jupyter_object( configuration=self.configuration, - login='jupyter1', + login=TEST_JUPYTER_SESSION_ID, home='jupyter_home1', pubkey=TEST_USER_PUB_KEY ) @@ -235,7 +284,7 @@ def test_add_jupyter_object_basic(self): mounts = self.configuration.daemon_conf['jupyter_mounts'] self.assertEqual(len(mounts), 1) mount = mounts[0] - self.assertEqual(mount.username, 'jupyter1') + self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) self.assertEqual(mount.home, 'jupyter_home1') # Convert saved paramiko.PKey back to openssh pub key format and check login_key = mount.public_key @@ -247,7 +296,7 @@ def test_add_jupyter_object_uses_creds_lock(self): fake_lock = _FakeLock() self.configuration.daemon_conf['creds_lock'] = fake_lock add_jupyter_object(configuration=self.configuration, - login='jupyter2', + login=OTHER_JUPYTER_SESSION_ID, home='jupyter_home2') self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) @@ -1064,6 +1113,355 @@ def test_lock_handling(self): self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) +class MigSharedGriddaemonsLogin__refresh_job_creds(MigTestCase): + """Unit tests for the refresh_job_creds helper.""" + + def _provide_configuration(self): + return 'testconfig' + + def before_each(self): + """Set up configuration and directories.""" + self.configuration.site_enable_jobs = True + self.configuration.site_enable_gdp = False + ensure_dirs_exist(self.configuration.user_home) + ensure_dirs_exist(self.configuration.mrsl_files_dir) + ensure_dirs_exist(self.configuration.sessid_to_mrsl_link_home) + self.configuration.daemon_conf = { + 'jobs': [], + 'time_stamp': 0, + 'creds_lock': threading.Lock(), + 'allow_publickey': True, + } + + # Ensure the user dir for the job user exists (for chroot validation) + self.job_user_home = os.path.join(self.configuration.user_home, + TEST_JOB_USER_DIR) + ensure_dirs_exist(self.job_user_home) + + def test_refresh_job_creds_invalid_protocol(self): + """Invalid protocol should return early with no changes.""" + conf, changed = refresh_job_creds(self.configuration, 'invalid', + TEST_JOB_ID) + self.assertEqual(changed, []) + self.assertEqual(conf['jobs'], []) + + def test_refresh_job_creds_invalid_job_id(self): + """Invalid job ID format should return early.""" + # possible_job_id usually checks for alphanumeric + dash/underscore + conf, changed = refresh_job_creds(self.configuration, 'sftp', + 'invalid@id') + self.assertEqual(changed, []) + + def test_refresh_job_creds_no_link(self): + """Missing symlink should report change (removal) if job existed, else nothing.""" + # Case 1: Job does not exist in conf, link missing -> no change + conf, changed = refresh_job_creds( + self.configuration, 'sftp', OTHER_JOB_ID) + self.assertEqual(changed, []) + + # Case 2: Job exists in conf, link missing -> removal + add_job_object(self.configuration, OTHER_JOB_ID, 'home', + pubkey=TEST_USER_PUB_KEY) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + OTHER_JOB_ID) + self.assertIn(OTHER_JOB_ID, changed) + self.assertEqual(conf['jobs'], []) + + def test_refresh_job_creds_link_unchanged(self): + """Unchanged link (mtime <= time_stamp) should return no changes.""" + job_dict = { + 'STATUS': 'EXECUTING', + 'SESSIONID': 'staticjob', + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + } + _create_job_mrsl_file(self, 'staticjob', job_dict) + + # Pre-populate conf with job having current timestamp + current_time = time.time() + self.configuration.daemon_conf['time_stamp'] = current_time + add_job_object(self.configuration, 'staticjob', TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY) + # Manually set last_update to now to simulate "fresh" + self.configuration.daemon_conf['jobs'][0].last_update = current_time + + conf, changed = refresh_job_creds( + self.configuration, 'sftp', 'staticjob') + self.assertEqual(changed, []) + self.assertEqual(len(conf['jobs']), 1) + + def test_refresh_job_creds_valid_job_added(self): + """Valid executing job with key should be added to jobs.""" + job_dict = { + 'STATUS': 'EXECUTING', + 'SESSIONID': TEST_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + } + _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) + + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) + + self.assertIn(TEST_JOB_ID, changed) + self.assertEqual(len(conf['jobs']), 1) + job = conf['jobs'][0] + self.assertEqual(job.username, TEST_JOB_ID) + self.assertEqual(job.home, TEST_JOB_USER_DIR) + self.assertEqual(job.ip_addr, '127.0.0.1') + self.assertIsNotNone(job.public_key) + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = job.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) + + def test_refresh_job_creds_non_executing_status(self): + """Job with non-EXECUTING status should be removed as inactive.""" + daemon_conf = self.configuration.daemon_conf + # Directly register test job as executing before test + job_dict = { + 'STATUS': 'EXECUTING', + 'SESSIONID': TEST_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + } + _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) + # Pre-add job to active jobs in conf jobs list + add_job_object(self.configuration, TEST_JOB_ID, TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY) + # NOTE: double check that job is in place for test + self.assertEqual(len(daemon_conf['jobs']), 1) + self.assertIn(TEST_JOB_ID, [i.username for i in daemon_conf['jobs']]) + + # Now mark finished and test that refresh removes it + job_dict['STATUS'] = 'FINISHED' + _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) + # NOTE: job is detected changed and should be removed + self.assertIn(TEST_JOB_ID, changed) + self.assertEqual(conf['jobs'], []) + + def test_refresh_job_creds_full_execution_cycle(self): + """Job going through states should be added and removed when done.""" + job_dict = { + 'STATUS': 'QUEUED', + 'SESSIONID': TEST_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + } + _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) + + # Verify that still only queued jobs don't get added + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) + # NOTE: job is detected changed but should remain inactive + self.assertIn(TEST_JOB_ID, changed) + self.assertEqual(conf['jobs'], []) + + # Verify that job gets added when changing to executing + job_dict['STATUS'] = 'EXECUTING' + _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) + # NOTE: job is detected changed and should be inserted + self.assertIn(TEST_JOB_ID, changed) + self.assertEqual(len(conf['jobs']), 1) + self.assertEqual(conf['jobs'][0].username, TEST_JOB_ID) + + # Verify that jobs get removed again when finished + job_dict['STATUS'] = 'FINISHED' + _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) + # NOTE: job is detected changed and should be removed + self.assertIn(TEST_JOB_ID, changed) + self.assertEqual(conf['jobs'], []) + + def test_refresh_job_creds_broken_key(self): + """Job with unparsable public key should be treated as inactive.""" + job_dict = { + 'STATUS': 'EXECUTING', + 'SESSIONID': OTHER_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': 'invalid-key-data', + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + } + _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) + + conf, changed = refresh_job_creds(self.configuration, 'sftp', + OTHER_JOB_ID) + self.assertIn(OTHER_JOB_ID, changed) + self.assertEqual(conf['jobs'], []) + + def test_refresh_job_creds_unresolvable_host(self): + """Job with unresolvable HOSTURL should be treated as inactive.""" + job_dict = { + 'STATUS': 'EXECUTING', + 'SESSIONID': OTHER_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'nonexistent.invalid.tld'}, + } + _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) + + # Mock socket to raise gaierror + original_gethostbyname_ex = socket.gethostbyname_ex + socket.gethostbyname_ex = lambda h: ( + _ for _ in ()).throw(socket.gaierror) + try: + conf, changed = refresh_job_creds(self.configuration, 'sftp', + OTHER_JOB_ID) + finally: + socket.gethostbyname_ex = original_gethostbyname_ex + + self.assertIn(OTHER_JOB_ID, changed) + self.assertEqual(conf['jobs'], []) + + +class MigSharedGriddaemonsLogin__refresh_jupyter_creds(MigTestCase): + """Unit tests for the refresh_jupyter_creds helper.""" + + def _provide_configuration(self): + return 'testconfig' + + def before_each(self): + """Set up configuration and directories.""" + self.configuration.site_enable_jupyter = True + self.configuration.site_enable_gdp = False + ensure_dirs_exist(self.configuration.user_home) + ensure_dirs_exist(self.configuration.jupyter_mount_files_dir) + ensure_dirs_exist(self.configuration.sessid_to_jupyter_mount_link_home) + self.configuration.daemon_conf = { + 'jupyter_mounts': [], + 'time_stamp': 0, + 'creds_lock': threading.Lock(), + 'allow_publickey': True, + } + + self.jupyter_user_home = os.path.join(self.configuration.user_home, + TEST_JOB_USER_DIR) + ensure_dirs_exist(self.jupyter_user_home) + + def test_refresh_jupyter_creds_invalid_protocol(self): + """Invalid protocol should return early.""" + conf, changed = refresh_jupyter_creds(self.configuration, 'invalid', + TEST_JUPYTER_SESSION_ID) + self.assertEqual(changed, []) + + def test_refresh_jupyter_creds_invalid_id(self): + """Invalid jupyter mount ID format should return early.""" + conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', + 'invalid@id') + self.assertEqual(changed, []) + + @unittest.skip("TODO: implement the mentioned clean up and enable next") + def test_refresh_jupyter_creds_no_link(self): + """Missing symlink should clean up existing mounts for that user_dir.""" + daemon_conf = self.configuration.daemon_conf + # Directly add a stale mount + add_jupyter_object(self.configuration, TEST_JUPYTER_SESSION_ID, + TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) + + # NOTE: double check that jupyter session is in place for test + self.assertEqual(len(daemon_conf['jupyter_mounts']), 1) + self.assertIn(TEST_JUPYTER_SESSION_ID, + [i.username for i in daemon_conf['jupyter_mounts']]) + + # Now test that refresh removes it + conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', + TEST_JUPYTER_SESSION_ID) + + # NOTE: mount is detected unchanged and mount should be removed + self.assertEqual(len(changed), 0) + self.assertEqual(conf['jupyter_mounts'], []) + + def test_refresh_jupyter_creds_valid_mount_added(self): + """Valid jupyter mount should be added.""" + jupyter_dict = { + 'SESSIONID': TEST_JUPYTER_SESSION_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + } + _create_jupyter_mount_file(self, TEST_JUPYTER_SESSION_ID, jupyter_dict) + + conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', + TEST_JUPYTER_SESSION_ID) + + self.assertIn(TEST_JUPYTER_SESSION_ID, changed) + self.assertEqual(len(conf['jupyter_mounts']), 1) + mount = conf['jupyter_mounts'][0] + self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) + self.assertEqual(mount.home, TEST_JOB_USER_DIR) + self.assertIsNotNone(mount.public_key) + # Convert saved paramiko.PKey back to openssh pub key format and check + login_key = mount.public_key + result = _parse_pkey_to_openssh_format(login_key) + self.assertEqual(result, TEST_USER_PUB_KEY) + + def test_refresh_jupyter_creds_purges_old_keys_same_home(self): + """Adding a new mount for same home should purge old mounts.""" + # Add stale mount with different session ID but same home + add_jupyter_object(self.configuration, OTHER_JUPYTER_SESSION_ID, + TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) + + jupyter_dict = { + 'SESSIONID': OTHER_JUPYTER_SESSION_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + } + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, + jupyter_dict) + + conf, changed = refresh_jupyter_creds( + self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) + + self.assertIn(OTHER_JUPYTER_SESSION_ID, changed) + # Only the new one should remain + self.assertEqual(len(conf['jupyter_mounts']), 1) + self.assertEqual(conf['jupyter_mounts'][0].username, + OTHER_JUPYTER_SESSION_ID) + + def test_refresh_jupyter_creds_broken_key(self): + """Unparsable key should result in no mount added.""" + jupyter_dict = { + 'SESSIONID': OTHER_JUPYTER_SESSION_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNTSSHPUBLICKEY': 'invalid-key', + } + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, + jupyter_dict) + + conf, changed = refresh_jupyter_creds( + self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) + self.assertEqual(changed, []) + self.assertEqual(conf['jupyter_mounts'], []) + + def test_refresh_jupyter_creds_missing_fields(self): + """Missing required fields in dict should result in no mount.""" + jupyter_dict = { + 'SESSIONID': OTHER_JUPYTER_SESSION_ID, + # Missing USER_CERT and MOUNTSSHPUBLICKEY + } + _create_jupyter_mount_file( + self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) + + conf, changed = refresh_jupyter_creds( + self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) + self.assertEqual(changed, []) + self.assertEqual(conf['jupyter_mounts'], []) + + class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): """Unit tests for the griddaemons login refresh_share_creds helper.""" @@ -1867,9 +2265,9 @@ def test_update_login_map_users(self): def test_update_login_map_jobs(self): """Verify login_map is updated correctly for changed jobs.""" # Create Login objects - job1 = Login( + test_job = Login( configuration=self.configuration, - username='job1', + username=TEST_JOB_ID, home='home1', password=None, digest=None, @@ -1879,9 +2277,9 @@ def test_update_login_map_jobs(self): ip_addr='1.2.3.4', user_dict=None ) - job2 = Login( + other_job = Login( configuration=self.configuration, - username='job2', + username=OTHER_JOB_ID, home='home2', password=None, digest=None, @@ -1892,21 +2290,21 @@ def test_update_login_map_jobs(self): user_dict=None ) # Populate jobs list - self.configuration.daemon_conf['jobs'] = [job1, job2] + self.configuration.daemon_conf['jobs'] = [test_job, other_job] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_jobs=['job1', 'job2'] + changed_jobs=[TEST_JOB_ID, OTHER_JOB_ID] ) # Verify login_map login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('job1', login_map) - self.assertIn('job2', login_map) - self.assertEqual(login_map['job1'], [job1]) - self.assertEqual(login_map['job2'], [job2]) + self.assertIn(TEST_JOB_ID, login_map) + self.assertIn(OTHER_JOB_ID, login_map) + self.assertEqual(login_map[TEST_JOB_ID], [test_job]) + self.assertEqual(login_map[OTHER_JOB_ID], [other_job]) def test_update_login_map_shares(self): """Verify login_map is updated correctly for changed shares.""" @@ -1955,9 +2353,9 @@ def test_update_login_map_shares(self): def test_update_login_map_jupyter(self): """Verify login_map is updated correctly for changed jupyter mounts.""" # Create Login objects - jupyter1 = Login( + test_session = Login( configuration=self.configuration, - username='jupyter1', + username=TEST_JUPYTER_SESSION_ID, home='home1', password=None, digest=None, @@ -1967,9 +2365,9 @@ def test_update_login_map_jupyter(self): ip_addr=None, user_dict=None ) - jupyter2 = Login( + other_session = Login( configuration=self.configuration, - username='jupyter2', + username=OTHER_JUPYTER_SESSION_ID, home='home2', password=None, digest=None, @@ -1980,21 +2378,22 @@ def test_update_login_map_jupyter(self): user_dict=None ) # Populate jupyter_mounts list - self.configuration.daemon_conf['jupyter_mounts'] = [jupyter1, jupyter2] + self.configuration.daemon_conf['jupyter_mounts'] = [ + test_session, other_session] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_jupyter=['jupyter1', 'jupyter2'] + changed_jupyter=[TEST_JUPYTER_SESSION_ID, OTHER_JUPYTER_SESSION_ID] ) # Verify login_map login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('jupyter1', login_map) - self.assertIn('jupyter2', login_map) - self.assertEqual(login_map['jupyter1'], [jupyter1]) - self.assertEqual(login_map['jupyter2'], [jupyter2]) + self.assertIn(TEST_JUPYTER_SESSION_ID, login_map) + self.assertIn(OTHER_JUPYTER_SESSION_ID, login_map) + self.assertEqual(login_map[TEST_JUPYTER_SESSION_ID], [test_session]) + self.assertEqual(login_map[OTHER_JUPYTER_SESSION_ID], [other_session]) def test_update_login_map_nonexistent(self): """Verify login_map is set to empty list for non-existent usernames.""" From 02314b85c112df4730777b44e21ce793f4bb5568 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 15:53:19 +0200 Subject: [PATCH 37/61] Rename test suite to fit griddaemons login coverage. --- tests/test_mig_shared_griddaemons.py | 2667 -------------------- tests/test_mig_shared_griddaemons_login.py | 1771 +++++++------ 2 files changed, 1026 insertions(+), 3412 deletions(-) delete mode 100644 tests/test_mig_shared_griddaemons.py diff --git a/tests/test_mig_shared_griddaemons.py b/tests/test_mig_shared_griddaemons.py deleted file mode 100644 index 09d6b9952..000000000 --- a/tests/test_mig_shared_griddaemons.py +++ /dev/null @@ -1,2667 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# --- BEGIN_HEADER --- -# -# test_mig_shared_griddaemons - unit tests for the griddaemons helper functions -# Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH -# -# This file is part of MiG. -# -# MiG is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# MiG is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# --- END_HEADER --- -# - -"""Unit tests for the griddaemons helper functions""" - -import binascii -import os -import pickle -import socket -import threading -import time -import unittest - -# Imports required for the unit test wrapping -from mig.shared.base import client_id_dir, distinguished_name_to_user -from mig.shared.defaults import ( - X509_USER_ID_FORMAT, UUID_USER_ID_FORMAT, - READ_WRITE_ACCESS, READ_ONLY_ACCESS, WRITE_ONLY_ACCESS, - keyword_auto -) -from mig.shared.useradm import ( - _ensure_dirs_needed_for_userdb, create_user # , generate_password_hash -) - -# Imports of the code under test -from mig.shared.griddaemons.login import ( - Login, add_job_object, add_jupyter_object, add_share_object, - add_user_object, get_creds_changes, get_job_changes, get_share_changes, - login_map_lookup, refresh_job_creds, refresh_jupyter_creds, - refresh_share_creds, refresh_user_creds, - update_login_map, update_user_objects -) - -# Imports required for the unit tests themselves -from tests.support import ( - ensure_dirs_exist, MigTestCase, temppath, UserAssertMixin -) -from tests.support.usersupp import ( - TEST_USER_DN -) - -TEST_USER_UUID = "UniqueUserIdForTestUser" -TEST_USER_SHORT_ID = "abc123@some.org" -TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] -TEST_RO_SHARE_ID = 'abcdef1234' -TEST_RW_SHARE_ID = 'klmnop4567' -TEST_WO_SHARE_ID = 'uvwxyz7890' - -# NOTE: job IDs and jupyter sessions are 64 chars with jobs limited to hex -TEST_JOB_ID = \ - "0419b45ebc1dedbdcb91fa6251035a2096758f5d700e15478b27a90734454107" -TEST_JOB_USER_DN = TEST_USER_DN -TEST_JOB_USER_DIR = client_id_dir(TEST_USER_DN) -OTHER_JOB_ID = \ - "162b1575f7f62baf5c830cc7956a438f7d003b11cddbf6d21cfdc4f598fbf7c1" -TEST_JUPYTER_SESSION_ID = \ - "ohNo4ii9geeyei3Jai8aif6gae6Eebiechai3chegh0moo9NieveKu3AC8ooshuo" -OTHER_JUPYTER_SESSION_ID = \ - "sah3quoh3zedaemoovoowahlumeitumohv4iekooPhek3Ieng6Apho4aw0aiPhef" - -# NOTE: this is a sample valid but unused ssh public key as it must be parsable -TEST_USER_PUB_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCuJrICshi7S2KhV03qvgNVOx5ejmHsswdGbvR34wf+eN23Ghq6OZhwGye2S+J6LPVFI3p4SCqxX4URnUM8BRAsiuvbf/+GQfE2pAO0C+/g4V3hhYbYzIyrtPsP1Hl8GioxvZD5nDoLEA4TWokDC4D7SRfv+NEkFLplyVBwHtpUunXBS/zXYdQ4lgk7u8HBBCMqUGbZHfCc+6ibFVn/5WS6vVokL+fSWtxg9tUVWqsS/xtDGPH1wbJUf1Dm3D58KmdX8ca73tBoScwH8qUQwEcyM1JtWtbv1BAZFb+Qk6SEe4GPRsn3I4AAgC7xtU3HKQsiqe8Fzpick/uI5PU+vguitcV/9+AASnGVZJ9M+a63UvlFFloEYcI1LwdZ03JYPQXfXCzJSYiA+pTX4/cf10G4rlxsque4m4OcuCwLKvpTWA/Lla+UJqYhdQW+m7mSizPRoDPgh8mOta1PQob2sGSw8rhqLfApptAPZ0mkN0QY3Dv3i3ItgpYGcPNVXVdjmhU=' -# NOTE: this is a sample valid but unused ssh password hash as it must be parsable -TEST_USER_PW_HASH = "PBKDF2$sha256$10000$XMZGaar/pU4PvWDr$w0dYjezF6JGtSiYPexyZMt3lM1234uxi" - - -def _provision_uuid_test_user(configuration, client_id, client_overrides=None): - """Helper to provision test users when UUID format is used""" - # TODO: merge something like this version into standard _provision_test_user? - # IMPORTANT: we need to use explicit create_user here for UUID format! - user_dict = distinguished_name_to_user(client_id) - # NOTE: generate unique and short id based on id to avoid test collisions - user_dict["unique_id"] = binascii.hexlify( - client_id.encode('utf8')).decode('ascii') - user_dict["short_id"] = binascii.hexlify( - user_dict["email"].encode('utf8')).decode('ascii') - user_dict["comment"] = "This is the user account comment" - user_dict["locality"] = "" - user_dict["organizational_unit"] = "" - user_dict["password"] = "" - user_dict["password_hash"] = "" - if client_overrides is not None: - user_dict.update(client_overrides) - - create_user( - user_dict, - configuration, - keyword_auto, - default_renew=True, - ask_renew=False, - ) - return user_dict - - -def _prepare_auth_files(home_path, auth_protos=None): - """Helper to create helper auth files for on eor more auth_protos. - If None is passed ssh, ftps and davs auth files will be made. - """ - auth_files = [] - if auth_protos is None: - auth_protos = ['davs', 'ftps', 'ssh'] - - # Create requested auth dirs with files - for auth in auth_protos: - # Create a .PROTO directory with authorized_X file(s) - dot_proto_dir = os.path.join(home_path, '.%s' % auth) - ensure_dirs_exist(dot_proto_dir) - if auth == 'ssh': - authkeys_path = os.path.join(dot_proto_dir, 'authorized_keys') - with open(authkeys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - auth_files.append(authkeys_path) - - authpasswords_path = os.path.join(dot_proto_dir, - 'authorized_passwords') - with open(authpasswords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - auth_files.append(authpasswords_path) - return auth_files - - -def _parse_pkey_to_openssh_format(paramiko_pkey): - """Convert a parsed pub key on paramiko PKey format to openssh format""" - return "%s %s" % (paramiko_pkey.get_name(), paramiko_pkey.get_base64()) - - -def _create_job_mrsl_file(test_case, session_id, job_dict): - """Helper to create a .mRSL pickle file and symlink.""" - mrsl_dir = test_case.configuration.sessid_to_mrsl_link_home - ensure_dirs_exist(mrsl_dir) - - # Create actual pickle file in a temp location - pickle_path = temppath('job_%s.pkl' % session_id, test_case) - with open(pickle_path, 'wb') as pkl_fd: - pickle.dump(job_dict, pkl_fd) - - # Create symlink - link_path = os.path.join(mrsl_dir, '%s.mRSL' % session_id) - if os.path.lexists(link_path): - os.remove(link_path) - os.symlink(pickle_path, link_path) - return link_path - - -def _create_jupyter_mount_file(test_case, session_id, jupyter_dict): - """Helper to create a .jupyter_mount pickle file and symlink.""" - mount_dir = test_case.configuration.sessid_to_jupyter_mount_link_home - ensure_dirs_exist(mount_dir) - - pickle_path = temppath('jupyter_%s.pkl' % session_id, test_case) - with open(pickle_path, 'wb') as pkl_fd: - pickle.dump(jupyter_dict, pkl_fd) - - link_path = os.path.join(mount_dir, '%s.jupyter_mount' % session_id) - if os.path.lexists(link_path): - os.remove(link_path) - os.symlink(pickle_path, link_path) - return link_path - - -class _FakeLock(object): - """Small lock double for various tests with optional locking.""" - - def __init__(self): - self.acquired = False - self.released = False - - def acquire(self): - self.acquired = True - - def release(self): - self.released = True - - -class MigSharedGriddaemonsLogin__add_job_object(MigTestCase): - """Unit tests for the griddaemons add_job_object helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test.""" - # Initialize daemon_conf with empty jobs list - self.configuration.daemon_conf = { - 'jobs': [], - 'creds_lock': threading.Lock() # Ensure lock is present for testing - } - - def test_add_job_object_basic(self): - """Verify that add_job_object adds a Login object to the jobs list.""" - # Call the helper - add_job_object( - configuration=self.configuration, - login=TEST_JOB_ID, - home='home1', - password=None, - pubkey=TEST_USER_PUB_KEY - ) - - # Verify the job was added - jobs = self.configuration.daemon_conf['jobs'] - self.assertEqual(len(jobs), 1) - job = jobs[0] - self.assertEqual(job.username, TEST_JOB_ID) - self.assertEqual(job.home, 'home1') - self.assertEqual(job.password, None) - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = job.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) - - def test_add_job_object_uses_creds_lock(self): - """Verify add_job_object acquires and releases creds_lock.""" - fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_job_object(configuration=self.configuration, - login=OTHER_JOB_ID, - home='job_home2') - self.assertTrue(fake_lock.acquired) - self.assertTrue(fake_lock.released) - - def test_add_job_object_missing_home(self): - """Verify that add_job_object uses login as home if home is None.""" - add_job_object( - configuration=self.configuration, - login='job3', - home=None - ) - job = self.configuration.daemon_conf['jobs'][0] - self.assertEqual(job.home, 'job3') - - -class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): - """Unit tests for the griddaemons add_jupyter_object helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test.""" - # Initialize daemon_conf with empty jupyter_mounts list - self.configuration.daemon_conf = { - 'jupyter_mounts': [], - 'creds_lock': threading.Lock() - } - - def test_add_jupyter_object_basic(self): - """Verify that add_jupyter_object adds a Login object to jupyter_mounts.""" - # Call the helper - add_jupyter_object( - configuration=self.configuration, - login=TEST_JUPYTER_SESSION_ID, - home='jupyter_home1', - pubkey=TEST_USER_PUB_KEY - ) - - # Verify the jupyter mount was added - mounts = self.configuration.daemon_conf['jupyter_mounts'] - self.assertEqual(len(mounts), 1) - mount = mounts[0] - self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) - self.assertEqual(mount.home, 'jupyter_home1') - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = mount.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) - - def test_add_jupyter_object_uses_creds_lock(self): - """Verify add_jupyter_object acquires and releases creds_lock.""" - fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_jupyter_object(configuration=self.configuration, - login=OTHER_JUPYTER_SESSION_ID, - home='jupyter_home2') - self.assertTrue(fake_lock.acquired) - self.assertTrue(fake_lock.released) - - def test_add_jupyter_object_missing_home(self): - """Verify that add_jupyter_object uses login as home if home is None.""" - add_jupyter_object( - configuration=self.configuration, - login='jupyter3', - home=None - ) - mount = self.configuration.daemon_conf['jupyter_mounts'][0] - self.assertEqual(mount.home, 'jupyter3') - - -class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): - """Unit tests for the add_user_object helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up daemon_conf for add_user_object tests.""" - self.configuration.daemon_conf = { - 'users': [], - 'creds_lock': threading.Lock() - } - - def test_add_user_object_appends_login(self): - """Verify add_user_object appends a Login to users.""" - user_dict = {'user_id': TEST_USER_DN} - - add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1', - password=TEST_USER_PW_HASH, - access=READ_WRITE_ACCESS, - user_dict=user_dict - ) - - self.assertEqual(len(self.configuration.daemon_conf['users']), 1) - login = self.configuration.daemon_conf['users'][0] - self.assertIsInstance(login, Login) - self.assertEqual(login.username, TEST_USER_EMAIL) - self.assertEqual(login.home, 'home1') - self.assertEqual(login.password, TEST_USER_PW_HASH) - self.assertIsNone(login.digest) - self.assertIsNone(login.public_key) - self.assertTrue(login.chroot) - self.assertEqual(login.access, READ_WRITE_ACCESS) - self.assertEqual(login.user_dict, user_dict) - - def test_add_user_object_adds_public_key_login(self): - """Verify add_user_object stores parsed public key logins.""" - add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1', - pubkey=TEST_USER_PUB_KEY, - access=READ_WRITE_ACCESS - ) - - login = self.configuration.daemon_conf['users'][0] - self.assertIsNotNone(login.public_key) - key_line = "%s %s" % (login.public_key.get_name(), - login.public_key.get_base64()) - self.assertEqual(key_line, TEST_USER_PUB_KEY) - - def test_add_user_object_adds_digest_login(self): - """Verify add_user_object stores digest logins.""" - add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1', - digest='digest-value', - access=READ_ONLY_ACCESS - ) - - login = self.configuration.daemon_conf['users'][0] - self.assertEqual(login.digest, 'digest-value') - self.assertIsNone(login.password) - self.assertIsNone(login.public_key) - self.assertEqual(login.access, READ_ONLY_ACCESS) - - def test_add_user_object_without_lock(self): - """Verify add_user_object works when creds_lock is not configured.""" - daemon_conf = self.configuration.daemon_conf - daemon_conf.pop('creds_lock') - - add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home='home1' - ) - - self.assertEqual(len(daemon_conf['users']), 1) - self.assertEqual(daemon_conf['users'][0].username, TEST_USER_EMAIL) - - def test_add_user_object_uses_creds_lock(self): - """Verify add_user_object acquires and releases creds_lock.""" - fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_user_object(configuration=self.configuration, - login=TEST_USER_EMAIL, - home='user_home2' - ) - self.assertTrue(fake_lock.acquired) - self.assertTrue(fake_lock.released) - - def test_add_user_object_uses_login_as_home_when_home_none(self): - """Verify add_user_object normalizes missing home to login name.""" - add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home=None - ) - - login = self.configuration.daemon_conf['users'][0] - self.assertEqual(login.home, TEST_USER_EMAIL) - - -class MigSharedGriddaemonsLogin__add_share_object(MigTestCase): - """Unit tests for the add_share_object helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up daemon_conf for add_share_object tests.""" - self.configuration.daemon_conf = { - 'shares': [], - 'creds_lock': threading.Lock() - } - - def test_add_share_object_appends_login(self): - """Verify add_share_object appends a Login to shares.""" - user_dict = {} - home_path = temppath('share_home', self) - ensure_dirs_exist(home_path) - login = Login( - configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS, - user_dict=user_dict - ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path, - password=login.password, - access=login.access - ) - self.assertEqual(len(self.configuration.daemon_conf['shares']), 1) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.username, TEST_RW_SHARE_ID) - self.assertEqual(share_login.home, home_path) - self.assertEqual(share_login.password, TEST_RW_SHARE_ID) - self.assertTrue(share_login.chroot) - self.assertEqual(share_login.access, READ_WRITE_ACCESS) - - def test_add_share_object_adds_public_key_login(self): - """Verify add_share_object stores parsed public key logins.""" - home_path = temppath('share_home_pubkey', self) - ensure_dirs_exist(home_path) - login = Login( - configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS - ) - add_share_object( - configuration=self.configuration, - login=login, - home=home_path, - pubkey=TEST_USER_PUB_KEY - ) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertIsNotNone(share_login.public_key) - key_line = "%s %s" % (share_login.public_key.get_name(), - share_login.public_key.get_base64()) - self.assertEqual(key_line, TEST_USER_PUB_KEY) - - def test_add_share_object_adds_digest_login(self): - """Verify add_share_object stores digest logins.""" - home_path = temppath('share_home_digest', self) - ensure_dirs_exist(home_path) - login = Login( - configuration=self.configuration, - username=TEST_RO_SHARE_ID, - home=home_path, - password=TEST_RO_SHARE_ID, - access=READ_ONLY_ACCESS - ) - add_share_object( - configuration=self.configuration, - login=login, - home=home_path, - digest='digest-value', - access=READ_ONLY_ACCESS - ) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.digest, 'digest-value') - self.assertIsNone(share_login.password) - self.assertIsNone(share_login.public_key) - self.assertEqual(share_login.access, READ_ONLY_ACCESS) - - def test_add_share_object_without_lock(self): - """Verify add_share_object works when creds_lock is not configured.""" - daemon_conf = self.configuration.daemon_conf - daemon_conf.pop('creds_lock') - home_path = temppath('share_home_nolock', self) - ensure_dirs_exist(home_path) - login = Login( - configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=home_path, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS - ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path) - self.assertEqual(len(daemon_conf['shares']), 1) - self.assertEqual(daemon_conf['shares'][0].username, TEST_RW_SHARE_ID) - - def test_add_share_object_uses_creds_lock(self): - """Verify add_share_object acquires and releases creds_lock.""" - fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_share_object(configuration=self.configuration, - login=TEST_RW_SHARE_ID, - home='share_home2') - self.assertTrue(fake_lock.acquired) - self.assertTrue(fake_lock.released) - - def test_add_share_object_uses_login_as_home_when_home_none(self): - """Verify add_share_object normalizes missing home to login name.""" - login = Login( - configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=None, - password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS - ) - add_share_object(configuration=self.configuration, - login=login.username, - home=None) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.home, TEST_RW_SHARE_ID) - - -class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): - """Unit tests for griddaemons login get_creds_changes function""" - - def _provide_configuration(self): - """Return a test configuration instance""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test""" - # Force X509 user id format - self.configuration.site_user_id_format = X509_USER_ID_FORMAT - - _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - - self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) - self.auth_keys_path, self.auth_passwords_path = auth_files - self.auth_digests_path = None - - def test_get_creds_changes_detects_new_files(self): - """Verify that new credential files are detected as changes""" - daemon_conf = self.configuration.daemon_conf - - # Create a dummy user with a last_update in the past - past_timestamp = time.time() - 3600 - dummy_user = Login( - configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - 'user', - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path - ) - - self.assertIn(self.auth_keys_path, changed_paths) - self.assertIn(self.auth_passwords_path, changed_paths) - # self.assertIn(self.auth_digests_path, changed_paths) - - def test_get_creds_changes_no_changes(self): - """Verify that unchanged credential files return an empty list""" - daemon_conf = self.configuration.daemon_conf - - # Set the file modification times to now - current_time = time.time() - - # Create a dummy user with last_update matching the file mtime - dummy_user = Login( - configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = current_time - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - TEST_USER_SHORT_ID, - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path - ) - - self.assertEqual(len(changed_paths), 0) - - -class MigSharedGriddaemonsLogin__get_creds_changes_uuid_user_id(MigTestCase): - """Unit tests for griddaemons login get_creds_changes function with UUID users""" - - def _provide_configuration(self): - """Return a test configuration instance""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test""" - # Force UUID user id format - self.configuration.site_user_id_format = UUID_USER_ID_FORMAT - # Ensure required directories exist - ensure_dirs_exist(self.configuration.mig_system_files) - _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - - user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) - self.test_user_id = user_id = user_dict['unique_id'] - client_dir = client_id_dir(TEST_USER_DN) - self.test_user_home = os.path.join(self.configuration.user_home, - user_id) - self.test_user_dir = os.path.basename(self.test_user_home) - self.test_user_home_x509 = os.path.join(self.configuration.user_home, - client_dir) - self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) - # Make sure X509 link are provisioned as well - if not os.path.islink(self.test_user_home_x509): - os.symlink(self.test_user_home, self.test_user_home_x509) - - auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) - self.auth_keys_path, self.auth_passwords_path = auth_files - self.auth_digests_path = None - - def test_get_creds_changes_detects_new_files(self): - """Verify that new credential files are detected as changes""" - daemon_conf = self.configuration.daemon_conf - - # Create a dummy user with a last_update in the past - past_timestamp = time.time() - 3600 - # NOTE: we use x509 alias over uuid home as in real runs - dummy_user = Login( - configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home_x509, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - 'user', - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path - ) - - self.assertIn(self.auth_keys_path, changed_paths) - self.assertIn(self.auth_passwords_path, changed_paths) - # self.assertIn(self.auth_digests_path, changed_paths) - - def test_get_creds_changes_no_changes(self): - """Verify that unchanged credential files return an empty list""" - daemon_conf = self.configuration.daemon_conf - - # Set the file modification times to now - current_time = time.time() - - # Create a dummy user with last_update matching the file mtime - # NOTE: we use x509 alias over uuid home as in real runs - dummy_user = Login( - configuration=self.configuration, - username=TEST_USER_SHORT_ID, - home=self.test_user_home_x509, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_user.last_update = current_time - daemon_conf['users'].append(dummy_user) - - changed_paths = get_creds_changes( - daemon_conf, - TEST_USER_SHORT_ID, - self.auth_keys_path, - self.auth_passwords_path, - self.auth_digests_path - ) - - self.assertEqual(len(changed_paths), 0) - - -class MigSharedGriddaemonsLogin__get_job_changes(MigTestCase): - """Unit tests for griddaemons login get_job_changes function""" - - def _provide_configuration(self): - """Return a test configuration instance""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test""" - _ensure_dirs_needed_for_userdb(self.configuration) - ensure_dirs_exist(self.configuration.sharelink_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['jobs'] = [] - - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - # self.auth_digests_path = temppath('authhorized_digests', self) - self.auth_digests_path = None - - # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - # with open(self.auth_digests_path, 'w') as creds_fd: - # creds_fd.write(TEST_USER_DIGEST) - - def test_get_job_changes_new_job(self): - """Verify that a new job mrsl file is detected as a change""" - daemon_conf = self.configuration.daemon_conf - - mrsl_path = temppath('test_job.mRSL', self) - with open(mrsl_path, 'w') as mrsl_fd: - mrsl_fd.write('test content') - - changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path - ) - - self.assertIn(mrsl_path, changed_paths) - - def test_get_job_changes_no_changes(self): - """Verify that unchanged job mrsl file returns an empty list""" - daemon_conf = self.configuration.daemon_conf - - mrsl_path = temppath('test_job.mRSL', self) - with open(mrsl_path, 'w') as mrsl_fd: - mrsl_fd.write('test content') - - # Create a dummy job with last_update matching file mtime - current_time = time.time() - dummy_job = Login( - configuration=self.configuration, - username='test_session_id', - home='job_home', - password=None, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_job.last_update = current_time - daemon_conf['jobs'].append(dummy_job) - - changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path - ) - - self.assertEqual(len(changed_paths), 0) - - def test_get_job_changes_missing_file(self): - """Verify that missing mrsl file is detected for existing job""" - daemon_conf = self.configuration.daemon_conf - - # Create a dummy job - dummy_job = Login( - configuration=self.configuration, - username='test_session_id', - home='job_home', - password=None, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - daemon_conf['jobs'].append(dummy_job) - - mrsl_path = temppath('missing_job.mRSL', self) - - changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path - ) - - self.assertIn(mrsl_path, changed_paths) - - -class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): - """Unit tests for griddaemons login get_share_changes helper function""" - - def _provide_configuration(self): - """Return a test configuration instance""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test""" - _ensure_dirs_needed_for_userdb(self.configuration) - self.ro_share_home = os.path.join(self.configuration.sharelink_home, - 'read-only') - self.rw_share_home = os.path.join(self.configuration.sharelink_home, - 'read-write') - self.wo_share_home = os.path.join(self.configuration.sharelink_home, - 'write-only') - ensure_dirs_exist(self.ro_share_home) - ensure_dirs_exist(self.rw_share_home) - ensure_dirs_exist(self.wo_share_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['shares'] = [] - - # TODO: enable and test unsafe digest auth, too? - # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False - - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - # self.auth_digests_path = temppath('authhorized_digests', self) - self.auth_digests_path = None - - # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - # with open(self.auth_digests_path, 'w') as creds_fd: - # creds_fd.write(TEST_USER_DIGEST) - - def test_get_share_changes_detects_updates(self): - """Verify that share link and key file changes are detected""" - daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True - - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') - ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join(user_shared_dir, '.ssh', - 'authorized_keys') - share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - # Create a dummy share with a last_update in the past - past_timestamp = time.time() - 3600 - dummy_share = Login( - configuration=self.configuration, - username=TEST_RO_SHARE_ID, - home='share_home', - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_share.last_update = past_timestamp - daemon_conf['shares'].append(dummy_share) - - changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - user_shared_keys - ) - - self.assertIn(share_link_path, changed_paths) - self.assertIn(user_shared_keys, changed_paths) - - def test_get_share_changes_new_share(self): - """Verify that a new share link is detected as a change""" - daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True - - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') - ensure_dirs_exist(user_shared_dir) - share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - self.auth_keys_path - ) - - self.assertIn(share_link_path, changed_paths) - self.assertIn(self.auth_keys_path, changed_paths) - - def test_get_share_changes_no_changes(self): - """Verify that unchanged share files return an empty list""" - daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True - - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') - ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join(user_shared_dir, '.ssh', - 'authorized_keys') - ensure_dirs_exist(user_shared_keys) - share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - # Create a dummy share with last_update matching file mtime - current_time = time.time() - dummy_share = Login( - configuration=self.configuration, - username=TEST_RO_SHARE_ID, - home='share_home', - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - dummy_share.last_update = current_time - daemon_conf['shares'].append(dummy_share) - - changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - user_shared_keys - ) - - self.assertEqual(len(changed_paths), 0) - - -class MigSharedGriddaemonsLogin__login_map_lookup(MigTestCase): - """Unit tests for the login_map_lookup function from griddaemons/login.py""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration with login_map and creds_lock.""" - # Common daemon configuration - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['login_map'] = {} - self.configuration.daemon_conf['creds_lock'] = threading.Lock() - - def test_user_exists_with_multiple_credentials(self): - """Verify login_map_lookup returns all credentials for a user.""" - # Create two Login objects for 'user1' - cred1 = Login( - configuration=self.configuration.daemon_conf, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - cred2 = Login( - configuration=self.configuration, - username='user1', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - # Populate login_map - daemon_conf = self.configuration.daemon_conf - daemon_conf['login_map']['user1'] = [cred1, cred2] - # Call the function - result = login_map_lookup(daemon_conf, 'user1') - # Assert - self.assertEqual(result, [cred1, cred2]) - - def test_user_exists_with_single_credential(self): - """Verify login_map_lookup returns one credential for a user.""" - cred = Login( - configuration=self.configuration, - username='user2', - home='home3', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - daemon_conf = self.configuration.daemon_conf - daemon_conf['login_map']['user2'] = [cred] - result = login_map_lookup(daemon_conf, 'user2') - self.assertEqual(result, [cred]) - - def test_user_does_not_exist(self): - """Verify login_map_lookup returns empty list for non-existent user.""" - daemon_conf = self.configuration.daemon_conf - result = login_map_lookup(daemon_conf, 'nosuchuser') - self.assertEqual(result, []) - - def test_lock_handling(self): - """Basic check that creds_lock is acquired and released.""" - # Simulate concurrent access (not fully testable in unit tests) - # This is a placeholder for thread-safety verification - daemon_conf = self.configuration.daemon_conf - cred = Login( - configuration=self.configuration, - username='user3', - home='home4', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - daemon_conf['login_map']['user3'] = [cred] - # Call the function (lock should be acquired/released internally) - result = login_map_lookup(daemon_conf, 'user3') - self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) - - -class MigSharedGriddaemonsLogin__refresh_job_creds(MigTestCase): - """Unit tests for the refresh_job_creds helper.""" - - def _provide_configuration(self): - return 'testconfig' - - def before_each(self): - """Set up configuration and directories.""" - self.configuration.site_enable_jobs = True - self.configuration.site_enable_gdp = False - ensure_dirs_exist(self.configuration.user_home) - ensure_dirs_exist(self.configuration.mrsl_files_dir) - ensure_dirs_exist(self.configuration.sessid_to_mrsl_link_home) - self.configuration.daemon_conf = { - 'jobs': [], - 'time_stamp': 0, - 'creds_lock': threading.Lock(), - 'allow_publickey': True, - } - - # Ensure the user dir for the job user exists (for chroot validation) - self.job_user_home = os.path.join(self.configuration.user_home, - TEST_JOB_USER_DIR) - ensure_dirs_exist(self.job_user_home) - - def test_refresh_job_creds_invalid_protocol(self): - """Invalid protocol should return early with no changes.""" - conf, changed = refresh_job_creds(self.configuration, 'invalid', - TEST_JOB_ID) - self.assertEqual(changed, []) - self.assertEqual(conf['jobs'], []) - - def test_refresh_job_creds_invalid_job_id(self): - """Invalid job ID format should return early.""" - # possible_job_id usually checks for alphanumeric + dash/underscore - conf, changed = refresh_job_creds(self.configuration, 'sftp', - 'invalid@id') - self.assertEqual(changed, []) - - def test_refresh_job_creds_no_link(self): - """Missing symlink should report change (removal) if job existed, else nothing.""" - # Case 1: Job does not exist in conf, link missing -> no change - conf, changed = refresh_job_creds( - self.configuration, 'sftp', OTHER_JOB_ID) - self.assertEqual(changed, []) - - # Case 2: Job exists in conf, link missing -> removal - add_job_object(self.configuration, OTHER_JOB_ID, 'home', - pubkey=TEST_USER_PUB_KEY) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - OTHER_JOB_ID) - self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) - - def test_refresh_job_creds_link_unchanged(self): - """Unchanged link (mtime <= time_stamp) should return no changes.""" - job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': 'staticjob', - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, - } - _create_job_mrsl_file(self, 'staticjob', job_dict) - - # Pre-populate conf with job having current timestamp - current_time = time.time() - self.configuration.daemon_conf['time_stamp'] = current_time - add_job_object(self.configuration, 'staticjob', TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY) - # Manually set last_update to now to simulate "fresh" - self.configuration.daemon_conf['jobs'][0].last_update = current_time - - conf, changed = refresh_job_creds( - self.configuration, 'sftp', 'staticjob') - self.assertEqual(changed, []) - self.assertEqual(len(conf['jobs']), 1) - - def test_refresh_job_creds_valid_job_added(self): - """Valid executing job with key should be added to jobs.""" - job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': TEST_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, - } - _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) - - self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(len(conf['jobs']), 1) - job = conf['jobs'][0] - self.assertEqual(job.username, TEST_JOB_ID) - self.assertEqual(job.home, TEST_JOB_USER_DIR) - self.assertEqual(job.ip_addr, '127.0.0.1') - self.assertIsNotNone(job.public_key) - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = job.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) - - def test_refresh_job_creds_non_executing_status(self): - """Job with non-EXECUTING status should be removed as inactive.""" - daemon_conf = self.configuration.daemon_conf - # Directly register test job as executing before test - job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': TEST_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, - } - _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - # Pre-add job to active jobs in conf jobs list - add_job_object(self.configuration, TEST_JOB_ID, TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY) - # NOTE: double check that job is in place for test - self.assertEqual(len(daemon_conf['jobs']), 1) - self.assertIn(TEST_JOB_ID, [i.username for i in daemon_conf['jobs']]) - - # Now mark finished and test that refresh removes it - job_dict['STATUS'] = 'FINISHED' - _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) - # NOTE: job is detected changed and should be removed - self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) - - def test_refresh_job_creds_full_execution_cycle(self): - """Job going through states should be added and removed when done.""" - job_dict = { - 'STATUS': 'QUEUED', - 'SESSIONID': TEST_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, - } - _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - - # Verify that still only queued jobs don't get added - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) - # NOTE: job is detected changed but should remain inactive - self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) - - # Verify that job gets added when changing to executing - job_dict['STATUS'] = 'EXECUTING' - _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) - # NOTE: job is detected changed and should be inserted - self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(len(conf['jobs']), 1) - self.assertEqual(conf['jobs'][0].username, TEST_JOB_ID) - - # Verify that jobs get removed again when finished - job_dict['STATUS'] = 'FINISHED' - _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) - # NOTE: job is detected changed and should be removed - self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) - - def test_refresh_job_creds_broken_key(self): - """Job with unparsable public key should be treated as inactive.""" - job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': OTHER_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': 'invalid-key-data', - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, - } - _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) - - conf, changed = refresh_job_creds(self.configuration, 'sftp', - OTHER_JOB_ID) - self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) - - def test_refresh_job_creds_unresolvable_host(self): - """Job with unresolvable HOSTURL should be treated as inactive.""" - job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': OTHER_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'nonexistent.invalid.tld'}, - } - _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) - - # Mock socket to raise gaierror - original_gethostbyname_ex = socket.gethostbyname_ex - socket.gethostbyname_ex = lambda h: ( - _ for _ in ()).throw(socket.gaierror) - try: - conf, changed = refresh_job_creds(self.configuration, 'sftp', - OTHER_JOB_ID) - finally: - socket.gethostbyname_ex = original_gethostbyname_ex - - self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) - - -class MigSharedGriddaemonsLogin__refresh_jupyter_creds(MigTestCase): - """Unit tests for the refresh_jupyter_creds helper.""" - - def _provide_configuration(self): - return 'testconfig' - - def before_each(self): - """Set up configuration and directories.""" - self.configuration.site_enable_jupyter = True - self.configuration.site_enable_gdp = False - ensure_dirs_exist(self.configuration.user_home) - ensure_dirs_exist(self.configuration.jupyter_mount_files_dir) - ensure_dirs_exist(self.configuration.sessid_to_jupyter_mount_link_home) - self.configuration.daemon_conf = { - 'jupyter_mounts': [], - 'time_stamp': 0, - 'creds_lock': threading.Lock(), - 'allow_publickey': True, - } - - self.jupyter_user_home = os.path.join(self.configuration.user_home, - TEST_JOB_USER_DIR) - ensure_dirs_exist(self.jupyter_user_home) - - def test_refresh_jupyter_creds_invalid_protocol(self): - """Invalid protocol should return early.""" - conf, changed = refresh_jupyter_creds(self.configuration, 'invalid', - TEST_JUPYTER_SESSION_ID) - self.assertEqual(changed, []) - - def test_refresh_jupyter_creds_invalid_id(self): - """Invalid jupyter mount ID format should return early.""" - conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', - 'invalid@id') - self.assertEqual(changed, []) - - @unittest.skip("TODO: implement the mentioned clean up and enable next") - def test_refresh_jupyter_creds_no_link(self): - """Missing symlink should clean up existing mounts for that user_dir.""" - daemon_conf = self.configuration.daemon_conf - # Directly add a stale mount - add_jupyter_object(self.configuration, TEST_JUPYTER_SESSION_ID, - TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) - - # NOTE: double check that jupyter session is in place for test - self.assertEqual(len(daemon_conf['jupyter_mounts']), 1) - self.assertIn(TEST_JUPYTER_SESSION_ID, - [i.username for i in daemon_conf['jupyter_mounts']]) - - # Now test that refresh removes it - conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', - TEST_JUPYTER_SESSION_ID) - - # NOTE: mount is detected unchanged and mount should be removed - self.assertEqual(len(changed), 0) - self.assertEqual(conf['jupyter_mounts'], []) - - def test_refresh_jupyter_creds_valid_mount_added(self): - """Valid jupyter mount should be added.""" - jupyter_dict = { - 'SESSIONID': TEST_JUPYTER_SESSION_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - } - _create_jupyter_mount_file(self, TEST_JUPYTER_SESSION_ID, jupyter_dict) - - conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', - TEST_JUPYTER_SESSION_ID) - - self.assertIn(TEST_JUPYTER_SESSION_ID, changed) - self.assertEqual(len(conf['jupyter_mounts']), 1) - mount = conf['jupyter_mounts'][0] - self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) - self.assertEqual(mount.home, TEST_JOB_USER_DIR) - self.assertIsNotNone(mount.public_key) - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = mount.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) - - def test_refresh_jupyter_creds_purges_old_keys_same_home(self): - """Adding a new mount for same home should purge old mounts.""" - # Add stale mount with different session ID but same home - add_jupyter_object(self.configuration, OTHER_JUPYTER_SESSION_ID, - TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) - - jupyter_dict = { - 'SESSIONID': OTHER_JUPYTER_SESSION_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, - jupyter_dict) - - conf, changed = refresh_jupyter_creds( - self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) - - self.assertIn(OTHER_JUPYTER_SESSION_ID, changed) - # Only the new one should remain - self.assertEqual(len(conf['jupyter_mounts']), 1) - self.assertEqual(conf['jupyter_mounts'][0].username, - OTHER_JUPYTER_SESSION_ID) - - def test_refresh_jupyter_creds_broken_key(self): - """Unparsable key should result in no mount added.""" - jupyter_dict = { - 'SESSIONID': OTHER_JUPYTER_SESSION_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNTSSHPUBLICKEY': 'invalid-key', - } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, - jupyter_dict) - - conf, changed = refresh_jupyter_creds( - self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) - self.assertEqual(changed, []) - self.assertEqual(conf['jupyter_mounts'], []) - - def test_refresh_jupyter_creds_missing_fields(self): - """Missing required fields in dict should result in no mount.""" - jupyter_dict = { - 'SESSIONID': OTHER_JUPYTER_SESSION_ID, - # Missing USER_CERT and MOUNTSSHPUBLICKEY - } - _create_jupyter_mount_file( - self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) - - conf, changed = refresh_jupyter_creds( - self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) - self.assertEqual(changed, []) - self.assertEqual(conf['jupyter_mounts'], []) - - -class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): - """Unit tests for the griddaemons login refresh_share_creds helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test.""" - # The base class already creates the required directory layout. - # Ensure the share‑link home exists – it is used by refresh_share_creds. - self.ro_share_home = os.path.join(self.configuration.sharelink_home, - 'read-only') - self.rw_share_home = os.path.join(self.configuration.sharelink_home, - 'read-write') - self.wo_share_home = os.path.join(self.configuration.sharelink_home, - 'write-only') - ensure_dirs_exist(self.ro_share_home) - ensure_dirs_exist(self.rw_share_home) - ensure_dirs_exist(self.wo_share_home) - - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['shares'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - - # Paths that the function will look at - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) - self.auth_digests_path = None - - # Create dummy credential files - with open(self.auth_keys_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: - creds_fd.write(TEST_USER_PW_HASH) - - def test_refresh_share_creds_adds_new_share(self): - """A new share link should be added to daemon_conf['shares'].""" - # Build a share link that points to a temporary user directory - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - # Call the function under test - # NOTE: only sftp access is supported for now - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # The share should now be present in the changed list - self.assertIn(TEST_RW_SHARE_ID, changed_shares) - - # Verify that a Login object was added to shares - share_login = [ - obj for obj in updated_conf['shares'] - if obj.username == TEST_RW_SHARE_ID - ] - self.assertEqual(len(share_login), 1) - - # The added object should contain the expected home directory - self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) - self.assertEqual(share_login[0].home, rel_share_home) - # TODO: check password hash, too? - # share_pw_hash = generate_password_hash(self.configuration, - # TEST_RW_SHARE_ID) - # self.assertEqual(share_login[0].password, share_pw_hash) - - def test_refresh_share_creds_adds_new_share_with_key(self): - """A new share link with key should be added twice to daemon_conf['shares'].""" - # Build a share link that points to a temporary user directory - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - _prepare_auth_files(user_shared_dir, ['ssh']) - - # Call the function under test - # NOTE: only sftp access is supported for now - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # The share should now be present twice in the changed list - self.assertIn(TEST_RW_SHARE_ID, changed_shares) - - # Verify that a Login object was added to shares - share_login = [ - obj for obj in updated_conf['shares'] - if obj.username == TEST_RW_SHARE_ID - ] - self.assertEqual(len(share_login), 2) - - # The added objects should contain the expected home directory - self.assertEqual(share_login[0].username, TEST_RW_SHARE_ID) - self.assertEqual(share_login[0].home, rel_share_home) - self.assertEqual(share_login[1].username, TEST_RW_SHARE_ID) - self.assertEqual(share_login[1].home, rel_share_home) - # TODO: check password hash, too? - # share_pw_hash = generate_password_hash(self.configuration, - # TEST_RW_SHARE_ID) - # self.assertEqual(share_login[0].password, share_pw_hash) - # Convert saved paramiko.PKey back to openssh pub key format and check - login_key = share_login[1].public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) - - def test_refresh_share_creds_no_changes(self): - """When the share link and its key file have not changed, the function - should return an empty changed_shares list.""" - daemon_conf = self.configuration.daemon_conf - - # Create a share link that already exists - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - # Populate shares with a dummy entry whose last_update matches - # the current file mtime – this simulates “no changes”. - current_time = time.time() - dummy_share = Login( - configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=rel_share_home, - password=TEST_RW_SHARE_ID, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None) - dummy_share.last_update = current_time - daemon_conf['shares'].append(dummy_share) - - # Run the function - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # No changes should be reported - self.assertEqual(len(changed_shares), 0) - - # The dummy entry should still be the only one present - self.assertEqual(len(updated_conf['shares']), 1) - - def test_refresh_share_creds_adds_readonly_share(self): - """Test that a read‑only share is added correctly""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.configuration.sharelink_home, - 'read-only', TEST_RO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RO_SHARE_ID, - share_modes=(READ_ONLY_ACCESS, ) - ) - - self.assertIn(TEST_RO_SHARE_ID, changed_shares) - share_login = [obj for obj in updated_conf['shares'] - if obj.username == TEST_RO_SHARE_ID] - self.assertEqual(len(share_login), 1) - self.assertEqual(share_login[0].home, rel_share_home) - - def test_refresh_share_creds_adds_writeonly_share(self): - """Test that a write‑only share is added correctly""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.wo_share_home, TEST_WO_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_WO_SHARE_ID, - share_modes=(WRITE_ONLY_ACCESS, ) - ) - - self.assertIn(TEST_WO_SHARE_ID, changed_shares) - share_login = [obj for obj in updated_conf['shares'] - if obj.username == TEST_WO_SHARE_ID] - self.assertEqual(len(share_login), 1) - self.assertEqual(share_login[0].home, rel_share_home) - - def test_refresh_share_creds_no_change_on_unchanged_link(self): - """Test that an unchanged share link does not trigger a change""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home) - ensure_dirs_exist(user_shared_dir) - - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(user_shared_dir, share_link_path) - - # Populate shares with a dummy entry whose last_update matches - # the current file mtime – this simulates “no changes”. - current_time = time.time() - dummy_share = Login( - configuration=self.configuration, - username=TEST_RW_SHARE_ID, - home=rel_share_home, - password=TEST_RW_SHARE_ID, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None) - dummy_share.last_update = current_time - self.configuration.daemon_conf['shares'].append(dummy_share) - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 1) - - def test_refresh_share_creds_detects_missing_link(self): - """Test that a missing share link is reported as a change""" - # No symlink created - share link is missing - missing_share_id = 'missing123' - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=missing_share_id, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # The function should still return an empty list because the link - # does not exist; no share is added. - self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 0) - - def test_refresh_share_creds_ignores_dead_link(self): - """Test that a dead share link is ignored""" - # Create a symlink that points nowhere - invalid_target = os.path.join(self.configuration.user_home, 'deadbeef') - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(invalid_target, share_link_path) - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # No share should be added because the link is invalid - self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 0) - - def test_refresh_share_creds_ignores_invalid_link(self): - """Test that an invalid (out of bounds) share link is ignored""" - # Create a symlink that points outside a user_home directory - invalid_target = self.configuration.certs_path - ensure_dirs_exist(invalid_target) - share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) - os.symlink(invalid_target, share_link_path) - - (updated_conf, changed_shares) = refresh_share_creds( - configuration=self.configuration, - protocol='sftp', - username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) - ) - - # No share should be added because the link is invalid but change - # is still reported as modified. - self.assertEqual(len(changed_shares), 1) - self.assertEqual(changed_shares[0], TEST_RW_SHARE_ID) - self.assertEqual(len(updated_conf['shares']), 0) - - -class MigSharedGriddaemonsLogin__refresh_user_creds(MigTestCase, UserAssertMixin): - """Unit tests for the griddaemons login refresh_user_creds helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test.""" - # Force X509 user id format - self.configuration.site_user_id_format = X509_USER_ID_FORMAT - # Ensure required directories exist - _ensure_dirs_needed_for_userdb(self.configuration) - self.expected_user_db_home = os.path.normpath( - self.configuration.user_db_home - ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" - ) - self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - self.test_user_dir = os.path.basename(self.test_user_home) - # Make sure alias link is provisioned as well - alias_link_path = os.path.join(self.configuration.user_home, - TEST_USER_EMAIL) - if not os.path.islink(alias_link_path): - os.symlink(self.test_user_home, alias_link_path) - - ALIAS_FIELD = 'email' - self.configuration.user_sftp_alias = ALIAS_FIELD - self.configuration.user_ftps_alias = ALIAS_FIELD - self.configuration.user_davs_alias = ALIAS_FIELD - - # Common daemon configuration - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = self.expected_user_db_file - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD - - def test_refresh_user_creds_ssh_protocol(self): - """Test refreshing user credentials for SSH protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) - - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least one login (the main username) and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) - - # Check that at least one login has the correct home directory - home_found = any(login.home == self.test_user_dir for login in - user_logins) - self.assertTrue(home_found) - - def test_refresh_user_creds_davs_protocol(self): - """Test refreshing user credentials for DAVS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['davs']) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='davs', - username=username - ) - - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - self.assertEqual(user_logins[0].home, self.test_user_dir) - - def test_refresh_user_creds_ftps_protocol(self): - """Test refreshing user credentials for FTPS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ftps']) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ftps', - username=username - ) - - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - self.assertEqual(user_logins[0].home, self.test_user_dir) - - def test_refresh_user_creds_https_protocol(self): - """Test refreshing user credentials for HTTPS protocol (uses user DB).""" - username = TEST_USER_EMAIL - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='https', - username=username - ) - - # The user alias should be in the changed list - self.assertIn(TEST_USER_EMAIL, changed_users) - - # Verify that Login objects were added to users for the username and its aliases - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least the main username and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) - - # Check that at least one login has the correct home directory - home_found = any(login.home == self.test_user_dir for login in - user_logins) - self.assertTrue(home_found) - - def test_refresh_user_creds_no_changes(self): - """Test that no changes are reported when credentials are unchanged.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) - - # Pre-populate the users list with a Login object that has - # last_update set to the current time (simulating no changes) - current_time = time.time() - dummy_user = Login( - configuration=self.configuration, - username=username, - home=self.test_user_home, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None) - dummy_user.last_update = current_time - self.configuration.daemon_conf['users'].append(dummy_user) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) - - # No changes should be reported - self.assertEqual(len(changed_users), 0) - # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf['users']), 1) - self.assertEqual(updated_conf['users'][0].username, username) - - def test_refresh_user_creds_missing_user(self): - """Test that a missing user is skipped.""" - username = 'nosuchuser' - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) - - # No changes should be reported because the home directory is missing - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - - def test_refresh_user_creds_invalid_protocol(self): - """Test that an invalid protocol returns early without changes.""" - username = TEST_USER_EMAIL - - # Call the function under test with an invalid protocol - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='invalid', - username=username - ) - - # No changes should be reported - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - -# TODO: merge X509 and UUID versions? - - -class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id(MigTestCase, UserAssertMixin): - """Unit tests for the griddaemons login refresh_user_creds helper with UUID users.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test.""" - # Force UUID user id format - self.configuration.site_user_id_format = UUID_USER_ID_FORMAT - # Ensure required directories exist - ensure_dirs_exist(self.configuration.mig_system_files) - _ensure_dirs_needed_for_userdb(self.configuration) - self.expected_user_db_home = os.path.normpath( - self.configuration.user_db_home - ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" - ) - # NOTE: we need to set a password_hash for https test to work with - overrides = {} - overrides["password_hash"] = TEST_USER_PW_HASH - - user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN, - overrides) - self.test_user_id = user_id = user_dict['unique_id'] - client_dir = client_id_dir(TEST_USER_DN) - self.test_user_home = os.path.join(self.configuration.user_home, - user_id) - self.test_user_dir = os.path.basename(self.test_user_home) - self.test_user_home_x509 = os.path.join(self.configuration.user_home, - client_dir) - self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) - # Make sure X509 and alias links are provisioned as well - if not os.path.islink(self.test_user_home_x509): - os.symlink(self.test_user_home, self.test_user_home_x509) - alias_link_path = os.path.join(self.configuration.user_home, - TEST_USER_EMAIL) - if not os.path.islink(alias_link_path): - os.symlink(self.test_user_home_x509, alias_link_path) - - ALIAS_FIELD = 'email' - self.configuration.user_sftp_alias = ALIAS_FIELD - self.configuration.user_ftps_alias = ALIAS_FIELD - self.configuration.user_davs_alias = ALIAS_FIELD - - # Common daemon configuration - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = self.expected_user_db_file - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD - - def test_refresh_user_creds_ssh_protocol(self): - """Test refreshing user credentials for SSH protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) - - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least one login (the main username) and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) - - # Check that at least one login has the correct home directory - # TODO: is this X509 dir what we want here or the UUID one? - home_found = any(login.home == self.test_user_dir_x509 for login in - user_logins) - self.assertTrue(home_found) - - def test_refresh_user_creds_davs_protocol(self): - """Test refreshing user credentials for DAVS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['davs']) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='davs', - username=username - ) - - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - # TODO: is this X509 dir what we want here or the UUID one? - self.assertEqual(user_logins[0].home, self.test_user_dir_x509) - - def test_refresh_user_creds_ftps_protocol(self): - """Test refreshing user credentials for FTPS protocol.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ftps']) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ftps', - username=username - ) - - # The user should be in the changed list - self.assertIn(username, changed_users) - - # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] - self.assertEqual(len(user_logins), 1) - # TODO: is this X509 dir what we want here or the UUID one? - self.assertEqual(user_logins[0].home, self.test_user_dir_x509) - - def test_refresh_user_creds_https_protocol(self): - """Test refreshing user credentials for HTTPS protocol (uses user DB).""" - username = TEST_USER_EMAIL - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='https', - username=username - ) - - # The user alias should be in the changed list - self.assertIn(TEST_USER_EMAIL, changed_users) - - # Verify that Login objects were added to users for the username and its aliases - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] - # We expect at least the main username and possibly aliases - self.assertGreaterEqual(len(user_logins), 1) - - # Check that at least one login has the correct home directory - # TODO: is this X509 dir what we want here or the UUID one? - home_found = any(login.home == self.test_user_dir_x509 for login in - user_logins) - self.assertTrue(home_found) - - def test_refresh_user_creds_no_changes(self): - """Test that no changes are reported when credentials are unchanged.""" - username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) - - # Pre-populate the users list with a Login object that has - # last_update set to the current time (simulating no changes) - current_time = time.time() - dummy_user = Login( - configuration=self.configuration, - username=username, - home=self.test_user_home, - password=TEST_USER_PW_HASH, - digest=None, - public_key=TEST_USER_PUB_KEY, - chroot=True, - access=None, - ip_addr=None, - user_dict=None) - dummy_user.last_update = current_time - self.configuration.daemon_conf['users'].append(dummy_user) - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) - - # No changes should be reported - self.assertEqual(len(changed_users), 0) - # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf['users']), 1) - self.assertEqual(updated_conf['users'][0].username, username) - - def test_refresh_user_creds_missing_user(self): - """Test that a missing user is skipped.""" - username = 'nosuchuser' - - # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username - ) - - # No changes should be reported because the home directory is missing - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - - def test_refresh_user_creds_invalid_protocol(self): - """Test that an invalid protocol returns early without changes.""" - username = TEST_USER_EMAIL - - # Call the function under test with an invalid protocol - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='invalid', - username=username - ) - - # No changes should be reported - self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) - - -class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): - """Unit tests for the update_login_map function from griddaemons/login.py""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration with login_map and creds_lock.""" - # Common daemon configuration - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['login_map'] = {} - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['jobs'] = [] - self.configuration.daemon_conf['shares'] = [] - self.configuration.daemon_conf['jupyter_mounts'] = [] - self.configuration.daemon_conf['creds_lock'] = threading.Lock() - - def test_update_login_map_users(self): - """Verify login_map is updated correctly for changed users.""" - # Create Login objects - user1 = Login( - configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - user2 = Login( - configuration=self.configuration, - username='user2', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - # Populate users list - self.configuration.daemon_conf['users'] = [user1, user2] - - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['user1', 'user2'] - ) - - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('user1', login_map) - self.assertIn('user2', login_map) - self.assertEqual(login_map['user1'], [user1]) - self.assertEqual(login_map['user2'], [user2]) - - def test_update_login_map_jobs(self): - """Verify login_map is updated correctly for changed jobs.""" - # Create Login objects - test_job = Login( - configuration=self.configuration, - username=TEST_JOB_ID, - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr='1.2.3.4', - user_dict=None - ) - other_job = Login( - configuration=self.configuration, - username=OTHER_JOB_ID, - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr='5.6.7.8', - user_dict=None - ) - # Populate jobs list - self.configuration.daemon_conf['jobs'] = [test_job, other_job] - - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_jobs=[TEST_JOB_ID, OTHER_JOB_ID] - ) - - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn(TEST_JOB_ID, login_map) - self.assertIn(OTHER_JOB_ID, login_map) - self.assertEqual(login_map[TEST_JOB_ID], [test_job]) - self.assertEqual(login_map[OTHER_JOB_ID], [other_job]) - - def test_update_login_map_shares(self): - """Verify login_map is updated correctly for changed shares.""" - # Create Login objects - share1 = Login( - configuration=self.configuration, - username='share1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - share2 = Login( - configuration=self.configuration, - username='share2', - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - # Populate shares list - self.configuration.daemon_conf['shares'] = [share1, share2] - - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_shares=['share1', 'share2'] - ) - - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('share1', login_map) - self.assertIn('share2', login_map) - self.assertEqual(login_map['share1'], [share1]) - self.assertEqual(login_map['share2'], [share2]) - - def test_update_login_map_jupyter(self): - """Verify login_map is updated correctly for changed jupyter mounts.""" - # Create Login objects - test_session = Login( - configuration=self.configuration, - username=TEST_JUPYTER_SESSION_ID, - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - other_session = Login( - configuration=self.configuration, - username=OTHER_JUPYTER_SESSION_ID, - home='home2', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - # Populate jupyter_mounts list - self.configuration.daemon_conf['jupyter_mounts'] = [ - test_session, other_session] - - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_jupyter=[TEST_JUPYTER_SESSION_ID, OTHER_JUPYTER_SESSION_ID] - ) - - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn(TEST_JUPYTER_SESSION_ID, login_map) - self.assertIn(OTHER_JUPYTER_SESSION_ID, login_map) - self.assertEqual(login_map[TEST_JUPYTER_SESSION_ID], [test_session]) - self.assertEqual(login_map[OTHER_JUPYTER_SESSION_ID], [other_session]) - - def test_update_login_map_nonexistent(self): - """Verify login_map is set to empty list for non-existent usernames.""" - # Populate users list with one user - user1 = Login( - configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - self.configuration.daemon_conf['users'] = [user1] - - # Call the function under test for a non-existent user - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['nonexistent'] - ) - - # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('nonexistent', login_map) - self.assertEqual(login_map['nonexistent'], []) - - def test_update_login_map_empty_lists(self): - """Verify login_map is not changed when changed lists are empty.""" - # Populate users list - user1 = Login( - configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - self.configuration.daemon_conf['users'] = [user1] - - # Call the function under test with empty lists - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=[], - changed_jobs=[], - changed_shares=[], - changed_jupyter=[] - ) - - # Verify login_map is unchanged (should be empty because we didn't update for 'user1') - login_map = self.configuration.daemon_conf['login_map'] - self.assertEqual(len(login_map), 0) - - def test_update_login_map_lock_handling(self): - """Basic check that creds_lock is acquired and released.""" - # Create a Login object - user1 = Login( - configuration=self.configuration, - username='user1', - home='home1', - password=None, - digest=None, - public_key=None, - chroot=True, - access=None, - ip_addr=None, - user_dict=None - ) - self.configuration.daemon_conf['users'] = [user1] - - # Call the function under test - update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['user1'] - ) - - # Verify login_map was updated (lock must have been acquired/released) - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('user1', login_map) - self.assertEqual(login_map['user1'], [user1]) - - -class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): - """Unit tests for the update_user_objects helper.""" - - def _provide_configuration(self): - """Return a test configuration instance.""" - return 'testconfig' - - def before_each(self): - """Set up test configuration and reset state before each test.""" - # Ensure required directories exist - _ensure_dirs_needed_for_userdb(self.configuration) - self.configuration.daemon_conf = {} - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = os.path.join( - self.configuration.user_db_home, "MiG-users.db") - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - - # Create a test user home - self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - self.test_user_dir = os.path.basename(self.test_user_home) - - # Create auth files for the user - self.ssh_auth_paths = _prepare_auth_files(self.test_user_home, ['ssh']) - self.ssh_auth_keys_path, self.ssh_auth_pw_path = self.ssh_auth_paths - - def test_update_user_objects_adds_passwords(self): - """Verify that update_user_objects adds Login objects for passwords.""" - daemon_conf = self.configuration.daemon_conf - # Extract the .PROTO/authorized_keys part from auth_keys_path - authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') - authkeys = authkeys.lstrip(os.sep) - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') - authpw = authpw.lstrip(os.sep) - authprotos = (authkeys, authpw, None) - user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, - TEST_USER_SHORT_ID, TEST_USER_EMAIL) - # Call the helper - update_user_objects( - configuration=self.configuration, - auth_file=authpw, - path=self.ssh_auth_pw_path, - user_vars=user_tuple, - auth_protos=authprotos, - private_auth_file=True - ) - - # Verify that three aliased pw logins were added - pw_logins = [u for u in daemon_conf['users'] if u.password is not None] - self.assertEqual(len(pw_logins), 3) - for entry in pw_logins: - self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, - TEST_USER_SHORT_ID)) - self.assertEqual(entry.home, self.test_user_dir) - self.assertEqual(entry.access, READ_WRITE_ACCESS) - self.assertEqual(entry.password, TEST_USER_PW_HASH) - - # Verify that no key logins were added - key_logins = [u for u in daemon_conf['users'] - if u.public_key is not None] - self.assertEqual(len(key_logins), 0) - # Verify that no digest logins were added - digest_logins = [u for u in daemon_conf['users'] - if u.digest is not None] - self.assertEqual(len(digest_logins), 0) - - def test_update_user_objects_adds_keys(self): - """Verify that update_user_objects adds Login objects for keys.""" - daemon_conf = self.configuration.daemon_conf - # Extract the .PROTO/authorized_keys part from auth_keys_path - authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') - authkeys = authkeys.lstrip(os.sep) - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') - authpw = authpw.lstrip(os.sep) - authprotos = (authkeys, authpw, None) - user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, - TEST_USER_SHORT_ID, TEST_USER_EMAIL) - # Call the helper - update_user_objects( - configuration=self.configuration, - auth_file=authkeys, - path=self.ssh_auth_keys_path, - user_vars=user_tuple, - auth_protos=authprotos, - private_auth_file=True - ) - - # Verify that three aliased key logins were added (dupe keys ignored) - key_logins = [u for u in daemon_conf['users'] if u.public_key is not - None] - self.assertEqual(len(key_logins), 3) - - for entry in key_logins: - self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, - TEST_USER_SHORT_ID)) - self.assertEqual(entry.home, self.test_user_dir) - self.assertEqual(entry.access, READ_WRITE_ACCESS) - login_key = entry.public_key - result = _parse_pkey_to_openssh_format(login_key) - self.assertEqual(result, TEST_USER_PUB_KEY) - - # Verify that no password logins were added - pw_logins = [u for u in daemon_conf['users'] if u.password is not None] - self.assertEqual(len(pw_logins), 0) - # Verify that no digest logins were added - digest_logins = [u for u in daemon_conf['users'] - if u.digest is not None] - self.assertEqual(len(digest_logins), 0) - - def test_update_user_objects_removes_old_entries(self): - """Verify that update_user_objects cleans old entries for the same user.""" - daemon_conf = self.configuration.daemon_conf - # Extract the .PROTO/authorized_passwords part from auth_pw_path - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') - authpw = authpw.lstrip(os.sep) - authprotos = (None, authpw, None) - user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, - None, None) - # Create a dummy user with a last_update in the past to remove in test - past_timestamp = time.time() - 3600 - dummy_user = Login( - configuration=self.configuration, - username=TEST_USER_EMAIL, - home=self.test_user_dir, - password=TEST_USER_PW_HASH, - access=READ_WRITE_ACCESS - ) - dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) - # Remove saved password - os.remove(self.ssh_auth_pw_path) - - # Call the helper - update_user_objects( - configuration=self.configuration, - auth_file=authpw, - path=self.ssh_auth_pw_path, - user_vars=user_tuple, - auth_protos=authprotos, - private_auth_file=True - ) - - # Old entry should be removed - usernames = [u.username for u in daemon_conf['users']] - self.assertNotIn(TEST_USER_DN, usernames, "Old entry was not removed") - - def test_update_user_objects_no_changes(self): - """Verify that calling update_user_objects with unchanged files does not duplicate entries.""" - daemon_conf = self.configuration.daemon_conf - # Extract the .PROTO/authorized_passwords part from auth_pw_path - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') - authpw = authpw.lstrip(os.sep) - authprotos = (None, authpw, None) - user_tuple = (TEST_USER_DN, None, self.test_user_dir, - None, None) - # First call to populate - update_user_objects( - configuration=self.configuration, - auth_file=authpw, - path=self.ssh_auth_pw_path, - user_vars=user_tuple, - auth_protos=authprotos, - private_auth_file=True - ) - initial_count = len(daemon_conf['users']) - - # Second call with same files - update_user_objects( - configuration=self.configuration, - auth_file=authpw, - path=self.ssh_auth_pw_path, - user_vars=user_tuple, - auth_protos=authprotos, - private_auth_file=True - ) - self.assertEqual(len(daemon_conf['users']), initial_count, - "Duplicate entries were added on second call") - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_mig_shared_griddaemons_login.py b/tests/test_mig_shared_griddaemons_login.py index ce3afc057..5d0a4a320 100644 --- a/tests/test_mig_shared_griddaemons_login.py +++ b/tests/test_mig_shared_griddaemons_login.py @@ -27,6 +27,7 @@ """Unit tests for the griddaemons login helper functions""" +import binascii import os import pickle import socket @@ -35,11 +36,14 @@ import unittest # Imports required for the unit test wrapping -from mig.shared.base import client_id_dir +from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( - READ_ONLY_ACCESS, + X509_USER_ID_FORMAT, + UUID_USER_ID_FORMAT, READ_WRITE_ACCESS, + READ_ONLY_ACCESS, WRITE_ONLY_ACCESS, + keyword_auto ) # Imports of the code under test @@ -62,7 +66,9 @@ ) # More imports required for the unit test wrapping -from mig.shared.useradm import _ensure_dirs_needed_for_userdb +from mig.shared.useradm import ( + _ensure_dirs_needed_for_userdb, create_user +) # Imports required for the unit tests themselves from tests.support import ( @@ -101,27 +107,56 @@ ) +def _provision_uuid_test_user(configuration, client_id, client_overrides=None): + """Helper to provision test users when UUID format is used""" + # TODO: merge something like this version into standard _provision_test_user? + # IMPORTANT: we need to use explicit create_user here for UUID format! + user_dict = distinguished_name_to_user(client_id) + # NOTE: generate unique and short id based on id to avoid test collisions + user_dict["unique_id"] = binascii.hexlify( + client_id.encode('utf8')).decode('ascii') + user_dict["short_id"] = binascii.hexlify( + user_dict["email"].encode('utf8')).decode('ascii') + user_dict["comment"] = "This is the user account comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = "" + if client_overrides is not None: + user_dict.update(client_overrides) + + create_user( + user_dict, + configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + return user_dict + + def _prepare_auth_files(home_path, auth_protos=None): """Helper to create helper auth files for on eor more auth_protos. If None is passed ssh, ftps and davs auth files will be made. """ auth_files = [] if auth_protos is None: - auth_protos = ["davs", "ftps", "ssh"] + auth_protos = ['davs', 'ftps', 'ssh'] # Create requested auth dirs with files for auth in auth_protos: # Create a .PROTO directory with authorized_X file(s) - dot_proto_dir = os.path.join(home_path, ".%s" % auth) + dot_proto_dir = os.path.join(home_path, '.%s' % auth) ensure_dirs_exist(dot_proto_dir) - if auth == "ssh": - authkeys_path = os.path.join(dot_proto_dir, "authorized_keys") - with open(authkeys_path, "w") as creds_fd: + if auth == 'ssh': + authkeys_path = os.path.join(dot_proto_dir, 'authorized_keys') + with open(authkeys_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) auth_files.append(authkeys_path) - authpasswords_path = os.path.join(dot_proto_dir, "authorized_passwords") - with open(authpasswords_path, "w") as creds_fd: + authpasswords_path = os.path.join(dot_proto_dir, + 'authorized_passwords') + with open(authpasswords_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PW_HASH) auth_files.append(authpasswords_path) return auth_files @@ -138,12 +173,12 @@ def _create_job_mrsl_file(test_case, session_id, job_dict): ensure_dirs_exist(mrsl_dir) # Create actual pickle file in a temp location - pickle_path = temppath("job_%s.pkl" % session_id, test_case) - with open(pickle_path, "wb") as pkl_fd: + pickle_path = temppath('job_%s.pkl' % session_id, test_case) + with open(pickle_path, 'wb') as pkl_fd: pickle.dump(job_dict, pkl_fd) # Create symlink - link_path = os.path.join(mrsl_dir, "%s.mRSL" % session_id) + link_path = os.path.join(mrsl_dir, '%s.mRSL' % session_id) if os.path.lexists(link_path): os.remove(link_path) os.symlink(pickle_path, link_path) @@ -155,11 +190,11 @@ def _create_jupyter_mount_file(test_case, session_id, jupyter_dict): mount_dir = test_case.configuration.sessid_to_jupyter_mount_link_home ensure_dirs_exist(mount_dir) - pickle_path = temppath("jupyter_%s.pkl" % session_id, test_case) - with open(pickle_path, "wb") as pkl_fd: + pickle_path = temppath('jupyter_%s.pkl' % session_id, test_case) + with open(pickle_path, 'wb') as pkl_fd: pickle.dump(jupyter_dict, pkl_fd) - link_path = os.path.join(mount_dir, "%s.jupyter_mount" % session_id) + link_path = os.path.join(mount_dir, '%s.jupyter_mount' % session_id) if os.path.lexists(link_path): os.remove(link_path) os.symlink(pickle_path, link_path) @@ -185,14 +220,14 @@ class MigSharedGriddaemonsLogin__add_job_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test.""" # Initialize daemon_conf with empty jobs list self.configuration.daemon_conf = { - "jobs": [], - "creds_lock": threading.Lock(), # Ensure lock is present for testing + 'jobs': [], + 'creds_lock': threading.Lock() # Ensure lock is present for testing } def test_add_job_object_basic(self): @@ -201,17 +236,17 @@ def test_add_job_object_basic(self): add_job_object( configuration=self.configuration, login=TEST_JOB_ID, - home="home1", + home='home1', password=None, - pubkey=TEST_USER_PUB_KEY, + pubkey=TEST_USER_PUB_KEY ) # Verify the job was added - jobs = self.configuration.daemon_conf["jobs"] + jobs = self.configuration.daemon_conf['jobs'] self.assertEqual(len(jobs), 1) job = jobs[0] self.assertEqual(job.username, TEST_JOB_ID) - self.assertEqual(job.home, "home1") + self.assertEqual(job.home, 'home1') self.assertEqual(job.password, None) # Convert saved paramiko.PKey back to openssh pub key format and check login_key = job.public_key @@ -221,22 +256,22 @@ def test_add_job_object_basic(self): def test_add_job_object_uses_creds_lock(self): """Verify add_job_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf["creds_lock"] = fake_lock - add_job_object( - configuration=self.configuration, - login=OTHER_JOB_ID, - home="job_home2", - ) + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_job_object(configuration=self.configuration, + login=OTHER_JOB_ID, + home='job_home2') self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) def test_add_job_object_missing_home(self): """Verify that add_job_object uses login as home if home is None.""" add_job_object( - configuration=self.configuration, login="job3", home=None + configuration=self.configuration, + login='job3', + home=None ) - job = self.configuration.daemon_conf["jobs"][0] - self.assertEqual(job.home, "job3") + job = self.configuration.daemon_conf['jobs'][0] + self.assertEqual(job.home, 'job3') class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): @@ -244,14 +279,14 @@ class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test.""" # Initialize daemon_conf with empty jupyter_mounts list self.configuration.daemon_conf = { - "jupyter_mounts": [], - "creds_lock": threading.Lock(), + 'jupyter_mounts': [], + 'creds_lock': threading.Lock() } def test_add_jupyter_object_basic(self): @@ -260,16 +295,16 @@ def test_add_jupyter_object_basic(self): add_jupyter_object( configuration=self.configuration, login=TEST_JUPYTER_SESSION_ID, - home="jupyter_home1", - pubkey=TEST_USER_PUB_KEY, + home='jupyter_home1', + pubkey=TEST_USER_PUB_KEY ) # Verify the jupyter mount was added - mounts = self.configuration.daemon_conf["jupyter_mounts"] + mounts = self.configuration.daemon_conf['jupyter_mounts'] self.assertEqual(len(mounts), 1) mount = mounts[0] self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) - self.assertEqual(mount.home, "jupyter_home1") + self.assertEqual(mount.home, 'jupyter_home1') # Convert saved paramiko.PKey back to openssh pub key format and check login_key = mount.public_key result = _parse_pkey_to_openssh_format(login_key) @@ -278,22 +313,22 @@ def test_add_jupyter_object_basic(self): def test_add_jupyter_object_uses_creds_lock(self): """Verify add_jupyter_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf["creds_lock"] = fake_lock - add_jupyter_object( - configuration=self.configuration, - login=OTHER_JUPYTER_SESSION_ID, - home="jupyter_home2", - ) + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_jupyter_object(configuration=self.configuration, + login=OTHER_JUPYTER_SESSION_ID, + home='jupyter_home2') self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) def test_add_jupyter_object_missing_home(self): """Verify that add_jupyter_object uses login as home if home is None.""" add_jupyter_object( - configuration=self.configuration, login="jupyter3", home=None + configuration=self.configuration, + login='jupyter3', + home=None ) - mount = self.configuration.daemon_conf["jupyter_mounts"][0] - self.assertEqual(mount.home, "jupyter3") + mount = self.configuration.daemon_conf['jupyter_mounts'][0] + self.assertEqual(mount.home, 'jupyter3') class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): @@ -301,33 +336,33 @@ class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up daemon_conf for add_user_object tests.""" self.configuration.daemon_conf = { - "users": [], - "creds_lock": threading.Lock(), + 'users': [], + 'creds_lock': threading.Lock() } def test_add_user_object_appends_login(self): """Verify add_user_object appends a Login to users.""" - user_dict = {"user_id": TEST_USER_DN} + user_dict = {'user_id': TEST_USER_DN} add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home="home1", + home='home1', password=TEST_USER_PW_HASH, access=READ_WRITE_ACCESS, - user_dict=user_dict, + user_dict=user_dict ) - self.assertEqual(len(self.configuration.daemon_conf["users"]), 1) - login = self.configuration.daemon_conf["users"][0] + self.assertEqual(len(self.configuration.daemon_conf['users']), 1) + login = self.configuration.daemon_conf['users'][0] self.assertIsInstance(login, Login) self.assertEqual(login.username, TEST_USER_EMAIL) - self.assertEqual(login.home, "home1") + self.assertEqual(login.home, 'home1') self.assertEqual(login.password, TEST_USER_PW_HASH) self.assertIsNone(login.digest) self.assertIsNone(login.public_key) @@ -340,17 +375,15 @@ def test_add_user_object_adds_public_key_login(self): add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home="home1", + home='home1', pubkey=TEST_USER_PUB_KEY, - access=READ_WRITE_ACCESS, + access=READ_WRITE_ACCESS ) - login = self.configuration.daemon_conf["users"][0] + login = self.configuration.daemon_conf['users'][0] self.assertIsNotNone(login.public_key) - key_line = "%s %s" % ( - login.public_key.get_name(), - login.public_key.get_base64(), - ) + key_line = "%s %s" % (login.public_key.get_name(), + login.public_key.get_base64()) self.assertEqual(key_line, TEST_USER_PUB_KEY) def test_add_user_object_adds_digest_login(self): @@ -358,13 +391,13 @@ def test_add_user_object_adds_digest_login(self): add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home="home1", - digest="digest-value", - access=READ_ONLY_ACCESS, + home='home1', + digest='digest-value', + access=READ_ONLY_ACCESS ) - login = self.configuration.daemon_conf["users"][0] - self.assertEqual(login.digest, "digest-value") + login = self.configuration.daemon_conf['users'][0] + self.assertEqual(login.digest, 'digest-value') self.assertIsNone(login.password) self.assertIsNone(login.public_key) self.assertEqual(login.access, READ_ONLY_ACCESS) @@ -372,36 +405,37 @@ def test_add_user_object_adds_digest_login(self): def test_add_user_object_without_lock(self): """Verify add_user_object works when creds_lock is not configured.""" daemon_conf = self.configuration.daemon_conf - daemon_conf.pop("creds_lock") + daemon_conf.pop('creds_lock') add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home="home1", + home='home1' ) - self.assertEqual(len(daemon_conf["users"]), 1) - self.assertEqual(daemon_conf["users"][0].username, TEST_USER_EMAIL) + self.assertEqual(len(daemon_conf['users']), 1) + self.assertEqual(daemon_conf['users'][0].username, TEST_USER_EMAIL) def test_add_user_object_uses_creds_lock(self): """Verify add_user_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf["creds_lock"] = fake_lock - add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home="user_home2", - ) + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_user_object(configuration=self.configuration, + login=TEST_USER_EMAIL, + home='user_home2' + ) self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) def test_add_user_object_uses_login_as_home_when_home_none(self): """Verify add_user_object normalizes missing home to login name.""" add_user_object( - configuration=self.configuration, login=TEST_USER_EMAIL, home=None + configuration=self.configuration, + login=TEST_USER_EMAIL, + home=None ) - login = self.configuration.daemon_conf["users"][0] + login = self.configuration.daemon_conf['users'][0] self.assertEqual(login.home, TEST_USER_EMAIL) @@ -410,19 +444,19 @@ class MigSharedGriddaemonsLogin__add_share_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up daemon_conf for add_share_object tests.""" self.configuration.daemon_conf = { - "shares": [], - "creds_lock": threading.Lock(), + 'shares': [], + 'creds_lock': threading.Lock() } def test_add_share_object_appends_login(self): """Verify add_share_object appends a Login to shares.""" user_dict = {} - home_path = temppath("share_home", self) + home_path = temppath('share_home', self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, @@ -430,17 +464,16 @@ def test_add_share_object_appends_login(self): home=home_path, password=TEST_RW_SHARE_ID, access=READ_WRITE_ACCESS, - user_dict=user_dict, - ) - add_share_object( - configuration=self.configuration, - login=login.username, - home=home_path, - password=login.password, - access=login.access, - ) - self.assertEqual(len(self.configuration.daemon_conf["shares"]), 1) - share_login = self.configuration.daemon_conf["shares"][0] + user_dict=user_dict + ) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path, + password=login.password, + access=login.access + ) + self.assertEqual(len(self.configuration.daemon_conf['shares']), 1) + share_login = self.configuration.daemon_conf['shares'][0] self.assertEqual(share_login.username, TEST_RW_SHARE_ID) self.assertEqual(share_login.home, home_path) self.assertEqual(share_login.password, TEST_RW_SHARE_ID) @@ -449,49 +482,47 @@ def test_add_share_object_appends_login(self): def test_add_share_object_adds_public_key_login(self): """Verify add_share_object stores parsed public key logins.""" - home_path = temppath("share_home_pubkey", self) + home_path = temppath('share_home_pubkey', self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, username=TEST_RW_SHARE_ID, home=home_path, password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS, + access=READ_WRITE_ACCESS ) add_share_object( configuration=self.configuration, login=login, home=home_path, - pubkey=TEST_USER_PUB_KEY, + pubkey=TEST_USER_PUB_KEY ) - share_login = self.configuration.daemon_conf["shares"][0] + share_login = self.configuration.daemon_conf['shares'][0] self.assertIsNotNone(share_login.public_key) - key_line = "%s %s" % ( - share_login.public_key.get_name(), - share_login.public_key.get_base64(), - ) + key_line = "%s %s" % (share_login.public_key.get_name(), + share_login.public_key.get_base64()) self.assertEqual(key_line, TEST_USER_PUB_KEY) def test_add_share_object_adds_digest_login(self): """Verify add_share_object stores digest logins.""" - home_path = temppath("share_home_digest", self) + home_path = temppath('share_home_digest', self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, username=TEST_RO_SHARE_ID, home=home_path, password=TEST_RO_SHARE_ID, - access=READ_ONLY_ACCESS, + access=READ_ONLY_ACCESS ) add_share_object( configuration=self.configuration, login=login, home=home_path, - digest="digest-value", - access=READ_ONLY_ACCESS, + digest='digest-value', + access=READ_ONLY_ACCESS ) - share_login = self.configuration.daemon_conf["shares"][0] - self.assertEqual(share_login.digest, "digest-value") + share_login = self.configuration.daemon_conf['shares'][0] + self.assertEqual(share_login.digest, 'digest-value') self.assertIsNone(share_login.password) self.assertIsNone(share_login.public_key) self.assertEqual(share_login.access, READ_ONLY_ACCESS) @@ -499,33 +530,29 @@ def test_add_share_object_adds_digest_login(self): def test_add_share_object_without_lock(self): """Verify add_share_object works when creds_lock is not configured.""" daemon_conf = self.configuration.daemon_conf - daemon_conf.pop("creds_lock") - home_path = temppath("share_home_nolock", self) + daemon_conf.pop('creds_lock') + home_path = temppath('share_home_nolock', self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, username=TEST_RW_SHARE_ID, home=home_path, password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS, - ) - add_share_object( - configuration=self.configuration, - login=login.username, - home=home_path, + access=READ_WRITE_ACCESS ) - self.assertEqual(len(daemon_conf["shares"]), 1) - self.assertEqual(daemon_conf["shares"][0].username, TEST_RW_SHARE_ID) + add_share_object(configuration=self.configuration, + login=login.username, + home=home_path) + self.assertEqual(len(daemon_conf['shares']), 1) + self.assertEqual(daemon_conf['shares'][0].username, TEST_RW_SHARE_ID) def test_add_share_object_uses_creds_lock(self): """Verify add_share_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf["creds_lock"] = fake_lock - add_share_object( - configuration=self.configuration, - login=TEST_RW_SHARE_ID, - home="share_home2", - ) + self.configuration.daemon_conf['creds_lock'] = fake_lock + add_share_object(configuration=self.configuration, + login=TEST_RW_SHARE_ID, + home='share_home2') self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) @@ -536,12 +563,12 @@ def test_add_share_object_uses_login_as_home_when_home_none(self): username=TEST_RW_SHARE_ID, home=None, password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS, - ) - add_share_object( - configuration=self.configuration, login=login.username, home=None + access=READ_WRITE_ACCESS ) - share_login = self.configuration.daemon_conf["shares"][0] + add_share_object(configuration=self.configuration, + login=login.username, + home=None) + share_login = self.configuration.daemon_conf['shares'][0] self.assertEqual(share_login.home, TEST_RW_SHARE_ID) @@ -550,25 +577,27 @@ class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test""" - # Ensure required directories exist + # Force X509 user id format + self.configuration.site_user_id_format = X509_USER_ID_FORMAT + _ensure_dirs_needed_for_userdb(self.configuration) ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["users"] = [] - self.configuration.daemon_conf["allow_publickey"] = True - self.configuration.daemon_conf["allow_password"] = True + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf['allow_digest'] = False self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - auth_files = _prepare_auth_files(self.test_user_home, ["ssh"]) + auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) self.auth_keys_path, self.auth_passwords_path = auth_files self.auth_digests_path = None @@ -588,17 +617,17 @@ def test_get_creds_changes_detects_new_files(self): chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) dummy_user.last_update = past_timestamp - daemon_conf["users"].append(dummy_user) + daemon_conf['users'].append(dummy_user) changed_paths = get_creds_changes( daemon_conf, - "user", + 'user', self.auth_keys_path, self.auth_passwords_path, - self.auth_digests_path, + self.auth_digests_path ) self.assertIn(self.auth_keys_path, changed_paths) @@ -623,17 +652,128 @@ def test_get_creds_changes_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) dummy_user.last_update = current_time - daemon_conf["users"].append(dummy_user) + daemon_conf['users'].append(dummy_user) changed_paths = get_creds_changes( daemon_conf, TEST_USER_SHORT_ID, self.auth_keys_path, self.auth_passwords_path, - self.auth_digests_path, + self.auth_digests_path + ) + + self.assertEqual(len(changed_paths), 0) + + +class MigSharedGriddaemonsLogin__get_creds_changes_uuid_user_id(MigTestCase): + """Unit tests for griddaemons login get_creds_changes function with UUID users""" + + def _provide_configuration(self): + """Return a test configuration instance""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test""" + # Force UUID user id format + self.configuration.site_user_id_format = UUID_USER_ID_FORMAT + # Ensure required directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(self.configuration) + ensure_dirs_exist(self.configuration.sharelink_home) + + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + # TODO: enable and test unsafe digest auth, too? + # self.configuration.daemon_conf['allow_digest'] = True + self.configuration.daemon_conf['allow_digest'] = False + + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) + self.test_user_id = user_id = user_dict['unique_id'] + client_dir = client_id_dir(TEST_USER_DN) + self.test_user_home = os.path.join(self.configuration.user_home, + user_id) + self.test_user_dir = os.path.basename(self.test_user_home) + self.test_user_home_x509 = os.path.join(self.configuration.user_home, + client_dir) + self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) + # Make sure X509 link are provisioned as well + if not os.path.islink(self.test_user_home_x509): + os.symlink(self.test_user_home, self.test_user_home_x509) + + auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + self.auth_keys_path, self.auth_passwords_path = auth_files + self.auth_digests_path = None + + def test_get_creds_changes_detects_new_files(self): + """Verify that new credential files are detected as changes""" + daemon_conf = self.configuration.daemon_conf + + # Create a dummy user with a last_update in the past + past_timestamp = time.time() - 3600 + # NOTE: we use x509 alias over uuid home as in real runs + dummy_user = Login( + configuration=self.configuration, + username=TEST_USER_SHORT_ID, + home=self.test_user_home_x509, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_user.last_update = past_timestamp + daemon_conf['users'].append(dummy_user) + + changed_paths = get_creds_changes( + daemon_conf, + 'user', + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path + ) + + self.assertIn(self.auth_keys_path, changed_paths) + self.assertIn(self.auth_passwords_path, changed_paths) + # self.assertIn(self.auth_digests_path, changed_paths) + + def test_get_creds_changes_no_changes(self): + """Verify that unchanged credential files return an empty list""" + daemon_conf = self.configuration.daemon_conf + + # Set the file modification times to now + current_time = time.time() + + # Create a dummy user with last_update matching the file mtime + # NOTE: we use x509 alias over uuid home as in real runs + dummy_user = Login( + configuration=self.configuration, + username=TEST_USER_SHORT_ID, + home=self.test_user_home_x509, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None + ) + dummy_user.last_update = current_time + daemon_conf['users'].append(dummy_user) + + changed_paths = get_creds_changes( + daemon_conf, + TEST_USER_SHORT_ID, + self.auth_keys_path, + self.auth_passwords_path, + self.auth_digests_path ) self.assertEqual(len(changed_paths), 0) @@ -644,7 +784,7 @@ class MigSharedGriddaemonsLogin__get_job_changes(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test""" @@ -652,22 +792,22 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["jobs"] = [] + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['jobs'] = [] # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf['allow_digest'] = False - self.auth_keys_path = temppath("authorized_keys", self) - self.auth_passwords_path = temppath("authorized_passwords", self) + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) # self.auth_digests_path = temppath('authhorized_digests', self) self.auth_digests_path = None # Create sample credential files - with open(self.auth_keys_path, "w") as creds_fd: + with open(self.auth_keys_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, "w") as creds_fd: + with open(self.auth_passwords_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PW_HASH) # with open(self.auth_digests_path, 'w') as creds_fd: # creds_fd.write(TEST_USER_DIGEST) @@ -676,12 +816,14 @@ def test_get_job_changes_new_job(self): """Verify that a new job mrsl file is detected as a change""" daemon_conf = self.configuration.daemon_conf - mrsl_path = temppath("test_job.mRSL", self) - with open(mrsl_path, "w") as mrsl_fd: - mrsl_fd.write("test content") + mrsl_path = temppath('test_job.mRSL', self) + with open(mrsl_path, 'w') as mrsl_fd: + mrsl_fd.write('test content') changed_paths = get_job_changes( - daemon_conf, "test_session_id", mrsl_path + daemon_conf, + 'test_session_id', + mrsl_path ) self.assertIn(mrsl_path, changed_paths) @@ -690,29 +832,31 @@ def test_get_job_changes_no_changes(self): """Verify that unchanged job mrsl file returns an empty list""" daemon_conf = self.configuration.daemon_conf - mrsl_path = temppath("test_job.mRSL", self) - with open(mrsl_path, "w") as mrsl_fd: - mrsl_fd.write("test content") + mrsl_path = temppath('test_job.mRSL', self) + with open(mrsl_path, 'w') as mrsl_fd: + mrsl_fd.write('test content') # Create a dummy job with last_update matching file mtime current_time = time.time() dummy_job = Login( configuration=self.configuration, - username="test_session_id", - home="job_home", + username='test_session_id', + home='job_home', password=None, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) dummy_job.last_update = current_time - daemon_conf["jobs"].append(dummy_job) + daemon_conf['jobs'].append(dummy_job) changed_paths = get_job_changes( - daemon_conf, "test_session_id", mrsl_path + daemon_conf, + 'test_session_id', + mrsl_path ) self.assertEqual(len(changed_paths), 0) @@ -724,22 +868,24 @@ def test_get_job_changes_missing_file(self): # Create a dummy job dummy_job = Login( configuration=self.configuration, - username="test_session_id", - home="job_home", + username='test_session_id', + home='job_home', password=None, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) - daemon_conf["jobs"].append(dummy_job) + daemon_conf['jobs'].append(dummy_job) - mrsl_path = temppath("missing_job.mRSL", self) + mrsl_path = temppath('missing_job.mRSL', self) changed_paths = get_job_changes( - daemon_conf, "test_session_id", mrsl_path + daemon_conf, + 'test_session_id', + mrsl_path ) self.assertIn(mrsl_path, changed_paths) @@ -750,41 +896,38 @@ class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test""" _ensure_dirs_needed_for_userdb(self.configuration) - self.ro_share_home = os.path.join( - self.configuration.sharelink_home, "read-only" - ) - self.rw_share_home = os.path.join( - self.configuration.sharelink_home, "read-write" - ) - self.wo_share_home = os.path.join( - self.configuration.sharelink_home, "write-only" - ) + self.ro_share_home = os.path.join(self.configuration.sharelink_home, + 'read-only') + self.rw_share_home = os.path.join(self.configuration.sharelink_home, + 'read-write') + self.wo_share_home = os.path.join(self.configuration.sharelink_home, + 'write-only') ensure_dirs_exist(self.ro_share_home) ensure_dirs_exist(self.rw_share_home) ensure_dirs_exist(self.wo_share_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["shares"] = [] + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['shares'] = [] # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf['allow_digest'] = False - self.auth_keys_path = temppath("authorized_keys", self) - self.auth_passwords_path = temppath("authorized_passwords", self) + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) # self.auth_digests_path = temppath('authhorized_digests', self) self.auth_digests_path = None # Create sample credential files - with open(self.auth_keys_path, "w") as creds_fd: + with open(self.auth_keys_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, "w") as creds_fd: + with open(self.auth_passwords_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PW_HASH) # with open(self.auth_digests_path, 'w') as creds_fd: # creds_fd.write(TEST_USER_DIGEST) @@ -792,15 +935,13 @@ def before_each(self): def test_get_share_changes_detects_updates(self): """Verify that share link and key file changes are detected""" daemon_conf = self.configuration.daemon_conf - daemon_conf["allow_publickey"] = True + daemon_conf['allow_publickey'] = True - user_shared_dir = os.path.join( - self.configuration.user_home, "TestUser", "shared", "data" - ) + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join( - user_shared_dir, ".ssh", "authorized_keys" - ) + user_shared_keys = os.path.join(user_shared_dir, '.ssh', + 'authorized_keys') share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) @@ -809,20 +950,23 @@ def test_get_share_changes_detects_updates(self): dummy_share = Login( configuration=self.configuration, username=TEST_RO_SHARE_ID, - home="share_home", + home='share_home', password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) dummy_share.last_update = past_timestamp - daemon_conf["shares"].append(dummy_share) + daemon_conf['shares'].append(dummy_share) changed_paths = get_share_changes( - daemon_conf, TEST_RO_SHARE_ID, share_link_path, user_shared_keys + daemon_conf, + TEST_RO_SHARE_ID, + share_link_path, + user_shared_keys ) self.assertIn(share_link_path, changed_paths) @@ -831,17 +975,19 @@ def test_get_share_changes_detects_updates(self): def test_get_share_changes_new_share(self): """Verify that a new share link is detected as a change""" daemon_conf = self.configuration.daemon_conf - daemon_conf["allow_publickey"] = True + daemon_conf['allow_publickey'] = True - user_shared_dir = os.path.join( - self.configuration.user_home, "TestUser", "shared", "data" - ) + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) changed_paths = get_share_changes( - daemon_conf, TEST_RO_SHARE_ID, share_link_path, self.auth_keys_path + daemon_conf, + TEST_RO_SHARE_ID, + share_link_path, + self.auth_keys_path ) self.assertIn(share_link_path, changed_paths) @@ -850,15 +996,13 @@ def test_get_share_changes_new_share(self): def test_get_share_changes_no_changes(self): """Verify that unchanged share files return an empty list""" daemon_conf = self.configuration.daemon_conf - daemon_conf["allow_publickey"] = True + daemon_conf['allow_publickey'] = True - user_shared_dir = os.path.join( - self.configuration.user_home, "TestUser", "shared", "data" - ) + user_shared_dir = os.path.join(self.configuration.user_home, + 'TestUser', 'shared', 'data') ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join( - user_shared_dir, ".ssh", "authorized_keys" - ) + user_shared_keys = os.path.join(user_shared_dir, '.ssh', + 'authorized_keys') ensure_dirs_exist(user_shared_keys) share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) @@ -868,20 +1012,23 @@ def test_get_share_changes_no_changes(self): dummy_share = Login( configuration=self.configuration, username=TEST_RO_SHARE_ID, - home="share_home", + home='share_home', password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) dummy_share.last_update = current_time - daemon_conf["shares"].append(dummy_share) + daemon_conf['shares'].append(dummy_share) changed_paths = get_share_changes( - daemon_conf, TEST_RO_SHARE_ID, share_link_path, user_shared_keys + daemon_conf, + TEST_RO_SHARE_ID, + share_link_path, + user_shared_keys ) self.assertEqual(len(changed_paths), 0) @@ -892,48 +1039,48 @@ class MigSharedGriddaemonsLogin__login_map_lookup(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration with login_map and creds_lock.""" # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["login_map"] = {} - self.configuration.daemon_conf["creds_lock"] = threading.Lock() + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['login_map'] = {} + self.configuration.daemon_conf['creds_lock'] = threading.Lock() def test_user_exists_with_multiple_credentials(self): """Verify login_map_lookup returns all credentials for a user.""" # Create two Login objects for 'user1' cred1 = Login( - configuration=self.configuration, - username="user1", - home="home1", + configuration=self.configuration.daemon_conf, + username='user1', + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) cred2 = Login( configuration=self.configuration, - username="user1", - home="home2", + username='user1', + home='home2', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) # Populate login_map daemon_conf = self.configuration.daemon_conf - daemon_conf["login_map"]["user1"] = [cred1, cred2] + daemon_conf['login_map']['user1'] = [cred1, cred2] # Call the function - result = login_map_lookup(daemon_conf, "user1") + result = login_map_lookup(daemon_conf, 'user1') # Assert self.assertEqual(result, [cred1, cred2]) @@ -941,25 +1088,25 @@ def test_user_exists_with_single_credential(self): """Verify login_map_lookup returns one credential for a user.""" cred = Login( configuration=self.configuration, - username="user2", - home="home3", + username='user2', + home='home3', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) daemon_conf = self.configuration.daemon_conf - daemon_conf["login_map"]["user2"] = [cred] - result = login_map_lookup(daemon_conf, "user2") + daemon_conf['login_map']['user2'] = [cred] + result = login_map_lookup(daemon_conf, 'user2') self.assertEqual(result, [cred]) def test_user_does_not_exist(self): """Verify login_map_lookup returns empty list for non-existent user.""" daemon_conf = self.configuration.daemon_conf - result = login_map_lookup(daemon_conf, "nosuchuser") + result = login_map_lookup(daemon_conf, 'nosuchuser') self.assertEqual(result, []) def test_lock_handling(self): @@ -969,27 +1116,27 @@ def test_lock_handling(self): daemon_conf = self.configuration.daemon_conf cred = Login( configuration=self.configuration, - username="user3", - home="home4", + username='user3', + home='home4', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) - daemon_conf["login_map"]["user3"] = [cred] + daemon_conf['login_map']['user3'] = [cred] # Call the function (lock should be acquired/released internally) - result = login_map_lookup(daemon_conf, "user3") - self.assertEqual(result, [daemon_conf["login_map"]["user3"][0]]) + result = login_map_lookup(daemon_conf, 'user3') + self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) class MigSharedGriddaemonsLogin__refresh_job_creds(MigTestCase): """Unit tests for the refresh_job_creds helper.""" def _provide_configuration(self): - return "testconfig" + return 'testconfig' def before_each(self): """Set up configuration and directories.""" @@ -999,104 +1146,92 @@ def before_each(self): ensure_dirs_exist(self.configuration.mrsl_files_dir) ensure_dirs_exist(self.configuration.sessid_to_mrsl_link_home) self.configuration.daemon_conf = { - "jobs": [], - "time_stamp": 0, - "creds_lock": threading.Lock(), - "allow_publickey": True, + 'jobs': [], + 'time_stamp': 0, + 'creds_lock': threading.Lock(), + 'allow_publickey': True, } # Ensure the user dir for the job user exists (for chroot validation) - self.job_user_home = os.path.join( - self.configuration.user_home, TEST_JOB_USER_DIR - ) + self.job_user_home = os.path.join(self.configuration.user_home, + TEST_JOB_USER_DIR) ensure_dirs_exist(self.job_user_home) def test_refresh_job_creds_invalid_protocol(self): """Invalid protocol should return early with no changes.""" - conf, changed = refresh_job_creds( - self.configuration, "invalid", TEST_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'invalid', + TEST_JOB_ID) self.assertEqual(changed, []) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) def test_refresh_job_creds_invalid_job_id(self): """Invalid job ID format should return early.""" # possible_job_id usually checks for alphanumeric + dash/underscore - conf, changed = refresh_job_creds( - self.configuration, "sftp", "invalid@id" - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + 'invalid@id') self.assertEqual(changed, []) def test_refresh_job_creds_no_link(self): """Missing symlink should report change (removal) if job existed, else nothing.""" # Case 1: Job does not exist in conf, link missing -> no change conf, changed = refresh_job_creds( - self.configuration, "sftp", OTHER_JOB_ID - ) + self.configuration, 'sftp', OTHER_JOB_ID) self.assertEqual(changed, []) # Case 2: Job exists in conf, link missing -> removal - add_job_object( - self.configuration, OTHER_JOB_ID, "home", pubkey=TEST_USER_PUB_KEY - ) - conf, changed = refresh_job_creds( - self.configuration, "sftp", OTHER_JOB_ID - ) + add_job_object(self.configuration, OTHER_JOB_ID, 'home', + pubkey=TEST_USER_PUB_KEY) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + OTHER_JOB_ID) self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) def test_refresh_job_creds_link_unchanged(self): """Unchanged link (mtime <= time_stamp) should return no changes.""" job_dict = { - "STATUS": "EXECUTING", - "SESSIONID": "staticjob", - "USER_CERT": TEST_JOB_USER_DN, - "MOUNT": "mount", - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, - "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, + 'STATUS': 'EXECUTING', + 'SESSIONID': 'staticjob', + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, } - _create_job_mrsl_file(self, "staticjob", job_dict) + _create_job_mrsl_file(self, 'staticjob', job_dict) # Pre-populate conf with job having current timestamp current_time = time.time() - self.configuration.daemon_conf["time_stamp"] = current_time - add_job_object( - self.configuration, - "staticjob", - TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY, - ) + self.configuration.daemon_conf['time_stamp'] = current_time + add_job_object(self.configuration, 'staticjob', TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY) # Manually set last_update to now to simulate "fresh" - self.configuration.daemon_conf["jobs"][0].last_update = current_time + self.configuration.daemon_conf['jobs'][0].last_update = current_time conf, changed = refresh_job_creds( - self.configuration, "sftp", "staticjob" - ) + self.configuration, 'sftp', 'staticjob') self.assertEqual(changed, []) - self.assertEqual(len(conf["jobs"]), 1) + self.assertEqual(len(conf['jobs']), 1) def test_refresh_job_creds_valid_job_added(self): """Valid executing job with key should be added to jobs.""" job_dict = { - "STATUS": "EXECUTING", - "SESSIONID": TEST_JOB_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNT": "mount", - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, - "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, + 'STATUS': 'EXECUTING', + 'SESSIONID': TEST_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, } _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds( - self.configuration, "sftp", TEST_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(len(conf["jobs"]), 1) - job = conf["jobs"][0] + self.assertEqual(len(conf['jobs']), 1) + job = conf['jobs'][0] self.assertEqual(job.username, TEST_JOB_ID) self.assertEqual(job.home, TEST_JOB_USER_DIR) - self.assertEqual(job.ip_addr, "127.0.0.1") + self.assertEqual(job.ip_addr, '127.0.0.1') self.assertIsNotNone(job.public_key) # Convert saved paramiko.PKey back to openssh pub key format and check login_key = job.public_key @@ -1108,127 +1243,116 @@ def test_refresh_job_creds_non_executing_status(self): daemon_conf = self.configuration.daemon_conf # Directly register test job as executing before test job_dict = { - "STATUS": "EXECUTING", - "SESSIONID": TEST_JOB_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNT": "mount", - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, - "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, + 'STATUS': 'EXECUTING', + 'SESSIONID': TEST_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, } _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) # Pre-add job to active jobs in conf jobs list - add_job_object( - self.configuration, - TEST_JOB_ID, - TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY, - ) + add_job_object(self.configuration, TEST_JOB_ID, TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY) # NOTE: double check that job is in place for test - self.assertEqual(len(daemon_conf["jobs"]), 1) - self.assertIn(TEST_JOB_ID, [i.username for i in daemon_conf["jobs"]]) + self.assertEqual(len(daemon_conf['jobs']), 1) + self.assertIn(TEST_JOB_ID, [i.username for i in daemon_conf['jobs']]) # Now mark finished and test that refresh removes it - job_dict["STATUS"] = "FINISHED" + job_dict['STATUS'] = 'FINISHED' _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds( - self.configuration, "sftp", TEST_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) # NOTE: job is detected changed and should be removed self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) def test_refresh_job_creds_full_execution_cycle(self): """Job going through states should be added and removed when done.""" job_dict = { - "STATUS": "QUEUED", - "SESSIONID": TEST_JOB_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNT": "mount", - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, - "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, + 'STATUS': 'QUEUED', + 'SESSIONID': TEST_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, } _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) # Verify that still only queued jobs don't get added - conf, changed = refresh_job_creds( - self.configuration, "sftp", TEST_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) # NOTE: job is detected changed but should remain inactive self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) # Verify that job gets added when changing to executing - job_dict["STATUS"] = "EXECUTING" + job_dict['STATUS'] = 'EXECUTING' _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds( - self.configuration, "sftp", TEST_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) # NOTE: job is detected changed and should be inserted self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(len(conf["jobs"]), 1) - self.assertEqual(conf["jobs"][0].username, TEST_JOB_ID) + self.assertEqual(len(conf['jobs']), 1) + self.assertEqual(conf['jobs'][0].username, TEST_JOB_ID) # Verify that jobs get removed again when finished - job_dict["STATUS"] = "FINISHED" + job_dict['STATUS'] = 'FINISHED' _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds( - self.configuration, "sftp", TEST_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + TEST_JOB_ID) # NOTE: job is detected changed and should be removed self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) def test_refresh_job_creds_broken_key(self): """Job with unparsable public key should be treated as inactive.""" job_dict = { - "STATUS": "EXECUTING", - "SESSIONID": OTHER_JOB_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNT": "mount", - "MOUNTSSHPUBLICKEY": "invalid-key-data", - "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, + 'STATUS': 'EXECUTING', + 'SESSIONID': OTHER_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': 'invalid-key-data', + 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, } _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) - conf, changed = refresh_job_creds( - self.configuration, "sftp", OTHER_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + OTHER_JOB_ID) self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) def test_refresh_job_creds_unresolvable_host(self): """Job with unresolvable HOSTURL should be treated as inactive.""" job_dict = { - "STATUS": "EXECUTING", - "SESSIONID": OTHER_JOB_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNT": "mount", - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, - "RESOURCE_CONFIG": {"HOSTURL": "nonexistent.invalid.tld"}, + 'STATUS': 'EXECUTING', + 'SESSIONID': OTHER_JOB_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNT': 'mount', + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + 'RESOURCE_CONFIG': {'HOSTURL': 'nonexistent.invalid.tld'}, } _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) # Mock socket to raise gaierror original_gethostbyname_ex = socket.gethostbyname_ex - socket.gethostbyname_ex = lambda h: (_ for _ in ()).throw( - socket.gaierror - ) + socket.gethostbyname_ex = lambda h: ( + _ for _ in ()).throw(socket.gaierror) try: - conf, changed = refresh_job_creds( - self.configuration, "sftp", OTHER_JOB_ID - ) + conf, changed = refresh_job_creds(self.configuration, 'sftp', + OTHER_JOB_ID) finally: socket.gethostbyname_ex = original_gethostbyname_ex self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf["jobs"], []) + self.assertEqual(conf['jobs'], []) class MigSharedGriddaemonsLogin__refresh_jupyter_creds(MigTestCase): """Unit tests for the refresh_jupyter_creds helper.""" def _provide_configuration(self): - return "testconfig" + return 'testconfig' def before_each(self): """Set up configuration and directories.""" @@ -1238,29 +1362,26 @@ def before_each(self): ensure_dirs_exist(self.configuration.jupyter_mount_files_dir) ensure_dirs_exist(self.configuration.sessid_to_jupyter_mount_link_home) self.configuration.daemon_conf = { - "jupyter_mounts": [], - "time_stamp": 0, - "creds_lock": threading.Lock(), - "allow_publickey": True, + 'jupyter_mounts': [], + 'time_stamp': 0, + 'creds_lock': threading.Lock(), + 'allow_publickey': True, } - self.jupyter_user_home = os.path.join( - self.configuration.user_home, TEST_JOB_USER_DIR - ) + self.jupyter_user_home = os.path.join(self.configuration.user_home, + TEST_JOB_USER_DIR) ensure_dirs_exist(self.jupyter_user_home) def test_refresh_jupyter_creds_invalid_protocol(self): """Invalid protocol should return early.""" - conf, changed = refresh_jupyter_creds( - self.configuration, "invalid", TEST_JUPYTER_SESSION_ID - ) + conf, changed = refresh_jupyter_creds(self.configuration, 'invalid', + TEST_JUPYTER_SESSION_ID) self.assertEqual(changed, []) def test_refresh_jupyter_creds_invalid_id(self): """Invalid jupyter mount ID format should return early.""" - conf, changed = refresh_jupyter_creds( - self.configuration, "sftp", "invalid@id" - ) + conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', + 'invalid@id') self.assertEqual(changed, []) @unittest.skip("TODO: implement the mentioned clean up and enable next") @@ -1268,45 +1389,37 @@ def test_refresh_jupyter_creds_no_link(self): """Missing symlink should clean up existing mounts for that user_dir.""" daemon_conf = self.configuration.daemon_conf # Directly add a stale mount - add_jupyter_object( - self.configuration, - TEST_JUPYTER_SESSION_ID, - TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY, - ) + add_jupyter_object(self.configuration, TEST_JUPYTER_SESSION_ID, + TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) # NOTE: double check that jupyter session is in place for test - self.assertEqual(len(daemon_conf["jupyter_mounts"]), 1) - self.assertIn( - TEST_JUPYTER_SESSION_ID, - [i.username for i in daemon_conf["jupyter_mounts"]], - ) + self.assertEqual(len(daemon_conf['jupyter_mounts']), 1) + self.assertIn(TEST_JUPYTER_SESSION_ID, + [i.username for i in daemon_conf['jupyter_mounts']]) # Now test that refresh removes it - conf, changed = refresh_jupyter_creds( - self.configuration, "sftp", TEST_JUPYTER_SESSION_ID - ) + conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', + TEST_JUPYTER_SESSION_ID) # NOTE: mount is detected unchanged and mount should be removed self.assertEqual(len(changed), 0) - self.assertEqual(conf["jupyter_mounts"], []) + self.assertEqual(conf['jupyter_mounts'], []) def test_refresh_jupyter_creds_valid_mount_added(self): """Valid jupyter mount should be added.""" jupyter_dict = { - "SESSIONID": TEST_JUPYTER_SESSION_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + 'SESSIONID': TEST_JUPYTER_SESSION_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, } _create_jupyter_mount_file(self, TEST_JUPYTER_SESSION_ID, jupyter_dict) - conf, changed = refresh_jupyter_creds( - self.configuration, "sftp", TEST_JUPYTER_SESSION_ID - ) + conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', + TEST_JUPYTER_SESSION_ID) self.assertIn(TEST_JUPYTER_SESSION_ID, changed) - self.assertEqual(len(conf["jupyter_mounts"]), 1) - mount = conf["jupyter_mounts"][0] + self.assertEqual(len(conf['jupyter_mounts']), 1) + mount = conf['jupyter_mounts'][0] self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) self.assertEqual(mount.home, TEST_JOB_USER_DIR) self.assertIsNotNone(mount.public_key) @@ -1318,59 +1431,54 @@ def test_refresh_jupyter_creds_valid_mount_added(self): def test_refresh_jupyter_creds_purges_old_keys_same_home(self): """Adding a new mount for same home should purge old mounts.""" # Add stale mount with different session ID but same home - add_jupyter_object( - self.configuration, - OTHER_JUPYTER_SESSION_ID, - TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY, - ) + add_jupyter_object(self.configuration, OTHER_JUPYTER_SESSION_ID, + TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) jupyter_dict = { - "SESSIONID": OTHER_JUPYTER_SESSION_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + 'SESSIONID': OTHER_JUPYTER_SESSION_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, + jupyter_dict) conf, changed = refresh_jupyter_creds( - self.configuration, "sftp", OTHER_JUPYTER_SESSION_ID - ) + self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) self.assertIn(OTHER_JUPYTER_SESSION_ID, changed) # Only the new one should remain - self.assertEqual(len(conf["jupyter_mounts"]), 1) - self.assertEqual( - conf["jupyter_mounts"][0].username, OTHER_JUPYTER_SESSION_ID - ) + self.assertEqual(len(conf['jupyter_mounts']), 1) + self.assertEqual(conf['jupyter_mounts'][0].username, + OTHER_JUPYTER_SESSION_ID) def test_refresh_jupyter_creds_broken_key(self): """Unparsable key should result in no mount added.""" jupyter_dict = { - "SESSIONID": OTHER_JUPYTER_SESSION_ID, - "USER_CERT": TEST_JOB_USER_DN, - "MOUNTSSHPUBLICKEY": "invalid-key", + 'SESSIONID': OTHER_JUPYTER_SESSION_ID, + 'USER_CERT': TEST_JOB_USER_DN, + 'MOUNTSSHPUBLICKEY': 'invalid-key', } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, + jupyter_dict) conf, changed = refresh_jupyter_creds( - self.configuration, "sftp", OTHER_JUPYTER_SESSION_ID - ) + self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) self.assertEqual(changed, []) - self.assertEqual(conf["jupyter_mounts"], []) + self.assertEqual(conf['jupyter_mounts'], []) def test_refresh_jupyter_creds_missing_fields(self): """Missing required fields in dict should result in no mount.""" jupyter_dict = { - "SESSIONID": OTHER_JUPYTER_SESSION_ID, + 'SESSIONID': OTHER_JUPYTER_SESSION_ID, # Missing USER_CERT and MOUNTSSHPUBLICKEY } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) + _create_jupyter_mount_file( + self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) conf, changed = refresh_jupyter_creds( - self.configuration, "sftp", OTHER_JUPYTER_SESSION_ID - ) + self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) self.assertEqual(changed, []) - self.assertEqual(conf["jupyter_mounts"], []) + self.assertEqual(conf['jupyter_mounts'], []) class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): @@ -1378,50 +1486,46 @@ class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test.""" # The base class already creates the required directory layout. # Ensure the share‑link home exists – it is used by refresh_share_creds. - self.ro_share_home = os.path.join( - self.configuration.sharelink_home, "read-only" - ) - self.rw_share_home = os.path.join( - self.configuration.sharelink_home, "read-write" - ) - self.wo_share_home = os.path.join( - self.configuration.sharelink_home, "write-only" - ) + self.ro_share_home = os.path.join(self.configuration.sharelink_home, + 'read-only') + self.rw_share_home = os.path.join(self.configuration.sharelink_home, + 'read-write') + self.wo_share_home = os.path.join(self.configuration.sharelink_home, + 'write-only') ensure_dirs_exist(self.ro_share_home) ensure_dirs_exist(self.rw_share_home) ensure_dirs_exist(self.wo_share_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["shares"] = [] - self.configuration.daemon_conf["allow_publickey"] = True - self.configuration.daemon_conf["allow_password"] = True - self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False # Paths that the function will look at - self.auth_keys_path = temppath("authorized_keys", self) - self.auth_passwords_path = temppath("authorized_passwords", self) + self.auth_keys_path = temppath('authorized_keys', self) + self.auth_passwords_path = temppath('authorized_passwords', self) self.auth_digests_path = None # Create dummy credential files - with open(self.auth_keys_path, "w") as creds_fd: + with open(self.auth_keys_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, "w") as creds_fd: + with open(self.auth_passwords_path, 'w') as creds_fd: creds_fd.write(TEST_USER_PW_HASH) def test_refresh_share_creds_adds_new_share(self): """A new share link should be added to daemon_conf['shares'].""" # Build a share link that points to a temporary user directory - rel_share_home = os.path.join("TestUser", "shared", "data") - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home - ) + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) @@ -1429,11 +1533,11 @@ def test_refresh_share_creds_adds_new_share(self): # Call the function under test # NOTE: only sftp access is supported for now - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) # The share should now be present in the changed list @@ -1441,8 +1545,7 @@ def test_refresh_share_creds_adds_new_share(self): # Verify that a Login object was added to shares share_login = [ - obj - for obj in updated_conf["shares"] + obj for obj in updated_conf['shares'] if obj.username == TEST_RW_SHARE_ID ] self.assertEqual(len(share_login), 1) @@ -1458,24 +1561,23 @@ def test_refresh_share_creds_adds_new_share(self): def test_refresh_share_creds_adds_new_share_with_key(self): """A new share link with key should be added twice to daemon_conf['shares'].""" # Build a share link that points to a temporary user directory - rel_share_home = os.path.join("TestUser", "shared", "data") - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home - ) + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) os.symlink(user_shared_dir, share_link_path) - _prepare_auth_files(user_shared_dir, ["ssh"]) + _prepare_auth_files(user_shared_dir, ['ssh']) # Call the function under test # NOTE: only sftp access is supported for now - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) # The share should now be present twice in the changed list @@ -1483,8 +1585,7 @@ def test_refresh_share_creds_adds_new_share_with_key(self): # Verify that a Login object was added to shares share_login = [ - obj - for obj in updated_conf["shares"] + obj for obj in updated_conf['shares'] if obj.username == TEST_RW_SHARE_ID ] self.assertEqual(len(share_login), 2) @@ -1509,10 +1610,9 @@ def test_refresh_share_creds_no_changes(self): daemon_conf = self.configuration.daemon_conf # Create a share link that already exists - rel_share_home = os.path.join("TestUser", "shared", "data") - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home - ) + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) @@ -1531,87 +1631,76 @@ def test_refresh_share_creds_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None, - ) + user_dict=None) dummy_share.last_update = current_time - daemon_conf["shares"].append(dummy_share) + daemon_conf['shares'].append(dummy_share) # Run the function - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) # No changes should be reported self.assertEqual(len(changed_shares), 0) # The dummy entry should still be the only one present - self.assertEqual(len(updated_conf["shares"]), 1) + self.assertEqual(len(updated_conf['shares']), 1) def test_refresh_share_creds_adds_readonly_share(self): """Test that a read‑only share is added correctly""" - rel_share_home = os.path.join("TestUser", "shared", "data") - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home - ) + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) ensure_dirs_exist(user_shared_dir) - share_link_path = os.path.join( - self.configuration.sharelink_home, "read-only", TEST_RO_SHARE_ID - ) + share_link_path = os.path.join(self.configuration.sharelink_home, + 'read-only', TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RO_SHARE_ID, - share_modes=(READ_ONLY_ACCESS,), + share_modes=(READ_ONLY_ACCESS, ) ) self.assertIn(TEST_RO_SHARE_ID, changed_shares) - share_login = [ - obj - for obj in updated_conf["shares"] - if obj.username == TEST_RO_SHARE_ID - ] + share_login = [obj for obj in updated_conf['shares'] + if obj.username == TEST_RO_SHARE_ID] self.assertEqual(len(share_login), 1) self.assertEqual(share_login[0].home, rel_share_home) def test_refresh_share_creds_adds_writeonly_share(self): """Test that a write‑only share is added correctly""" - rel_share_home = os.path.join("TestUser", "shared", "data") - user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home - ) + rel_share_home = os.path.join('TestUser', 'shared', 'data') + user_shared_dir = os.path.join(self.configuration.user_home, + rel_share_home) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.wo_share_home, TEST_WO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_WO_SHARE_ID, - share_modes=(WRITE_ONLY_ACCESS,), + share_modes=(WRITE_ONLY_ACCESS, ) ) self.assertIn(TEST_WO_SHARE_ID, changed_shares) - share_login = [ - obj - for obj in updated_conf["shares"] - if obj.username == TEST_WO_SHARE_ID - ] + share_login = [obj for obj in updated_conf['shares'] + if obj.username == TEST_WO_SHARE_ID] self.assertEqual(len(share_login), 1) self.assertEqual(share_login[0].home, rel_share_home) def test_refresh_share_creds_no_change_on_unchanged_link(self): """Test that an unchanged share link does not trigger a change""" - rel_share_home = os.path.join("TestUser", "shared", "data") + rel_share_home = os.path.join('TestUser', 'shared', 'data') user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home - ) + self.configuration.user_home, rel_share_home) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) @@ -1630,55 +1719,54 @@ def test_refresh_share_creds_no_change_on_unchanged_link(self): chroot=True, access=None, ip_addr=None, - user_dict=None, - ) + user_dict=None) dummy_share.last_update = current_time - self.configuration.daemon_conf["shares"].append(dummy_share) + self.configuration.daemon_conf['shares'].append(dummy_share) - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf["shares"]), 1) + self.assertEqual(len(updated_conf['shares']), 1) - def test_refresh_share_creds_detects_and_ignores_missing_link(self): - """Test that a completely missing share link is just ignored""" + def test_refresh_share_creds_detects_missing_link(self): + """Test that a missing share link is reported as a change""" # No symlink created - share link is missing - missing_share_id = "missing123" + missing_share_id = 'missing123' - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=missing_share_id, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) # The function should still return an empty list because the link # does not exist; no share is added. self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf["shares"]), 0) + self.assertEqual(len(updated_conf['shares']), 0) def test_refresh_share_creds_ignores_dead_link(self): """Test that a dead share link is ignored""" # Create a symlink that points nowhere - invalid_target = os.path.join(self.configuration.user_home, "deadbeef") + invalid_target = os.path.join(self.configuration.user_home, 'deadbeef') share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) os.symlink(invalid_target, share_link_path) - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) # No share should be added because the link is invalid self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf["shares"]), 0) + self.assertEqual(len(updated_conf['shares']), 0) def test_refresh_share_creds_ignores_invalid_link(self): """Test that an invalid (out of bounds) share link is ignored""" @@ -1688,31 +1776,31 @@ def test_refresh_share_creds_ignores_invalid_link(self): share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) os.symlink(invalid_target, share_link_path) - updated_conf, changed_shares = refresh_share_creds( + (updated_conf, changed_shares) = refresh_share_creds( configuration=self.configuration, - protocol="sftp", + protocol='sftp', username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS,), + share_modes=(READ_WRITE_ACCESS, ) ) # No share should be added because the link is invalid but change # is still reported as modified. self.assertEqual(len(changed_shares), 1) self.assertEqual(changed_shares[0], TEST_RW_SHARE_ID) - self.assertEqual(len(updated_conf["shares"]), 0) + self.assertEqual(len(updated_conf['shares']), 0) -class MigSharedGriddaemonsLogin__refresh_user_creds( - MigTestCase, UserAssertMixin -): +class MigSharedGriddaemonsLogin__refresh_user_creds(MigTestCase, UserAssertMixin): """Unit tests for the griddaemons login refresh_user_creds helper.""" def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test.""" + # Force X509 user id format + self.configuration.site_user_id_format = X509_USER_ID_FORMAT # Ensure required directories exist _ensure_dirs_needed_for_userdb(self.configuration) self.expected_user_db_home = os.path.normpath( @@ -1724,95 +1812,93 @@ def before_each(self): self.test_user_home = self._provision_test_user(self, TEST_USER_DN) self.test_user_dir = os.path.basename(self.test_user_home) # Make sure alias link is provisioned as well - alias_link_path = os.path.join( - self.configuration.user_home, TEST_USER_EMAIL - ) + alias_link_path = os.path.join(self.configuration.user_home, + TEST_USER_EMAIL) if not os.path.islink(alias_link_path): os.symlink(self.test_user_home, alias_link_path) - ALIAS_FIELD = "email" + ALIAS_FIELD = 'email' self.configuration.user_sftp_alias = ALIAS_FIELD self.configuration.user_ftps_alias = ALIAS_FIELD self.configuration.user_davs_alias = ALIAS_FIELD # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["users"] = [] - self.configuration.daemon_conf["root_dir"] = ( - self.configuration.user_home - ) - self.configuration.daemon_conf["db_path"] = self.expected_user_db_file - self.configuration.daemon_conf["allow_publickey"] = True - self.configuration.daemon_conf["allow_password"] = True - self.configuration.daemon_conf["allow_digest"] = False - self.configuration.daemon_conf["user_alias"] = ALIAS_FIELD + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD def test_refresh_user_creds_ssh_protocol(self): """Test refreshing user credentials for SSH protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ["ssh"]) + _prepare_auth_files(self.test_user_home, ['ssh']) # Call the function under test - updated_conf, changed_users = refresh_user_creds( - configuration=self.configuration, protocol="ssh", username=username + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [ - obj - for obj in updated_conf["users"] - if obj.username == TEST_USER_DN or obj.username == username - ] + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] # We expect at least one login (the main username) and possibly aliases self.assertGreaterEqual(len(user_logins), 1) # Check that at least one login has the correct home directory - home_found = any( - login.home == self.test_user_dir for login in user_logins - ) + home_found = any(login.home == self.test_user_dir for login in + user_logins) self.assertTrue(home_found) def test_refresh_user_creds_davs_protocol(self): """Test refreshing user credentials for DAVS protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ["davs"]) + _prepare_auth_files(self.test_user_home, ['davs']) # Call the function under test - updated_conf, changed_users = refresh_user_creds( - configuration=self.configuration, protocol="davs", username=username + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='davs', + username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [ - obj for obj in updated_conf["users"] if obj.username == username - ] + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] self.assertEqual(len(user_logins), 1) self.assertEqual(user_logins[0].home, self.test_user_dir) def test_refresh_user_creds_ftps_protocol(self): """Test refreshing user credentials for FTPS protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ["ftps"]) + _prepare_auth_files(self.test_user_home, ['ftps']) # Call the function under test - updated_conf, changed_users = refresh_user_creds( - configuration=self.configuration, protocol="ftps", username=username + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ftps', + username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [ - obj for obj in updated_conf["users"] if obj.username == username - ] + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] self.assertEqual(len(user_logins), 1) self.assertEqual(user_logins[0].home, self.test_user_dir) @@ -1821,34 +1907,31 @@ def test_refresh_user_creds_https_protocol(self): username = TEST_USER_EMAIL # Call the function under test - updated_conf, changed_users = refresh_user_creds( + (updated_conf, changed_users) = refresh_user_creds( configuration=self.configuration, - protocol="https", - username=username, + protocol='https', + username=username ) # The user alias should be in the changed list self.assertIn(TEST_USER_EMAIL, changed_users) # Verify that Login objects were added to users for the username and its aliases - user_logins = [ - obj - for obj in updated_conf["users"] - if obj.username == TEST_USER_DN or obj.username == username - ] + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] # We expect at least the main username and possibly aliases self.assertGreaterEqual(len(user_logins), 1) # Check that at least one login has the correct home directory - home_found = any( - login.home == self.test_user_dir for login in user_logins - ) + home_found = any(login.home == self.test_user_dir for login in + user_logins) self.assertTrue(home_found) def test_refresh_user_creds_no_changes(self): """Test that no changes are reported when credentials are unchanged.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ["ssh"]) + _prepare_auth_files(self.test_user_home, ['ssh']) # Pre-populate the users list with a Login object that has # last_update set to the current time (simulating no changes) @@ -1863,49 +1946,277 @@ def test_refresh_user_creds_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None, - ) + user_dict=None) dummy_user.last_update = current_time - self.configuration.daemon_conf["users"].append(dummy_user) + self.configuration.daemon_conf['users'].append(dummy_user) # Call the function under test - updated_conf, changed_users = refresh_user_creds( - configuration=self.configuration, protocol="ssh", username=username + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username ) # No changes should be reported self.assertEqual(len(changed_users), 0) # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf["users"]), 1) - self.assertEqual(updated_conf["users"][0].username, username) + self.assertEqual(len(updated_conf['users']), 1) + self.assertEqual(updated_conf['users'][0].username, username) def test_refresh_user_creds_missing_user(self): """Test that a missing user is skipped.""" - username = "nosuchuser" + username = 'nosuchuser' # Call the function under test - updated_conf, changed_users = refresh_user_creds( - configuration=self.configuration, protocol="ssh", username=username + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username ) # No changes should be reported because the home directory is missing self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf["users"]), 0) + self.assertEqual(len(updated_conf['users']), 0) def test_refresh_user_creds_invalid_protocol(self): """Test that an invalid protocol returns early without changes.""" username = TEST_USER_EMAIL # Call the function under test with an invalid protocol - updated_conf, changed_users = refresh_user_creds( + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='invalid', + username=username + ) + + # No changes should be reported + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) + +# TODO: merge X509 and UUID versions? + + +class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id(MigTestCase, UserAssertMixin): + """Unit tests for the griddaemons login refresh_user_creds helper with UUID users.""" + + def _provide_configuration(self): + """Return a test configuration instance.""" + return 'testconfig' + + def before_each(self): + """Set up test configuration and reset state before each test.""" + # Force UUID user id format + self.configuration.site_user_id_format = UUID_USER_ID_FORMAT + # Ensure required directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(self.configuration) + self.expected_user_db_home = os.path.normpath( + self.configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + # NOTE: we need to set a password_hash for https test to work with + overrides = {} + overrides["password_hash"] = TEST_USER_PW_HASH + + user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN, + overrides) + self.test_user_id = user_id = user_dict['unique_id'] + client_dir = client_id_dir(TEST_USER_DN) + self.test_user_home = os.path.join(self.configuration.user_home, + user_id) + self.test_user_dir = os.path.basename(self.test_user_home) + self.test_user_home_x509 = os.path.join(self.configuration.user_home, + client_dir) + self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) + # Make sure X509 and alias links are provisioned as well + if not os.path.islink(self.test_user_home_x509): + os.symlink(self.test_user_home, self.test_user_home_x509) + alias_link_path = os.path.join(self.configuration.user_home, + TEST_USER_EMAIL) + if not os.path.islink(alias_link_path): + os.symlink(self.test_user_home_x509, alias_link_path) + + ALIAS_FIELD = 'email' + self.configuration.user_sftp_alias = ALIAS_FIELD + self.configuration.user_ftps_alias = ALIAS_FIELD + self.configuration.user_davs_alias = ALIAS_FIELD + + # Common daemon configuration + self.configuration.daemon_conf = {} + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = self.expected_user_db_file + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD + + def test_refresh_user_creds_ssh_protocol(self): + """Test refreshing user credentials for SSH protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least one login (the main username) and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + # TODO: is this X509 dir what we want here or the UUID one? + home_found = any(login.home == self.test_user_dir_x509 for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_davs_protocol(self): + """Test refreshing user credentials for DAVS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['davs']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='davs', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + # TODO: is this X509 dir what we want here or the UUID one? + self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + + def test_refresh_user_creds_ftps_protocol(self): + """Test refreshing user credentials for FTPS protocol.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ftps']) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ftps', + username=username + ) + + # The user should be in the changed list + self.assertIn(username, changed_users) + + # Verify that Login objects were added to users + user_logins = [obj for obj in updated_conf['users'] + if obj.username == username] + self.assertEqual(len(user_logins), 1) + # TODO: is this X509 dir what we want here or the UUID one? + self.assertEqual(user_logins[0].home, self.test_user_dir_x509) + + def test_refresh_user_creds_https_protocol(self): + """Test refreshing user credentials for HTTPS protocol (uses user DB).""" + username = TEST_USER_EMAIL + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='https', + username=username + ) + + # The user alias should be in the changed list + self.assertIn(TEST_USER_EMAIL, changed_users) + + # Verify that Login objects were added to users for the username and its aliases + user_logins = [obj for obj in updated_conf['users'] + if obj.username == TEST_USER_DN or + obj.username == username] + # We expect at least the main username and possibly aliases + self.assertGreaterEqual(len(user_logins), 1) + + # Check that at least one login has the correct home directory + # TODO: is this X509 dir what we want here or the UUID one? + home_found = any(login.home == self.test_user_dir_x509 for login in + user_logins) + self.assertTrue(home_found) + + def test_refresh_user_creds_no_changes(self): + """Test that no changes are reported when credentials are unchanged.""" + username = TEST_USER_EMAIL + _prepare_auth_files(self.test_user_home, ['ssh']) + + # Pre-populate the users list with a Login object that has + # last_update set to the current time (simulating no changes) + current_time = time.time() + dummy_user = Login( configuration=self.configuration, - protocol="invalid", username=username, + home=self.test_user_home, + password=TEST_USER_PW_HASH, + digest=None, + public_key=TEST_USER_PUB_KEY, + chroot=True, + access=None, + ip_addr=None, + user_dict=None) + dummy_user.last_update = current_time + self.configuration.daemon_conf['users'].append(dummy_user) + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username ) # No changes should be reported self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf["users"]), 0) + # The user list should still contain only our dummy user + self.assertEqual(len(updated_conf['users']), 1) + self.assertEqual(updated_conf['users'][0].username, username) + + def test_refresh_user_creds_missing_user(self): + """Test that a missing user is skipped.""" + username = 'nosuchuser' + + # Call the function under test + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='ssh', + username=username + ) + + # No changes should be reported because the home directory is missing + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) + + def test_refresh_user_creds_invalid_protocol(self): + """Test that an invalid protocol returns early without changes.""" + username = TEST_USER_EMAIL + + # Call the function under test with an invalid protocol + (updated_conf, changed_users) = refresh_user_creds( + configuration=self.configuration, + protocol='invalid', + username=username + ) + + # No changes should be reported + self.assertEqual(len(changed_users), 0) + self.assertEqual(len(updated_conf['users']), 0) class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): @@ -1913,62 +2224,62 @@ class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration with login_map and creds_lock.""" # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf["time_stamp"] = 0 - self.configuration.daemon_conf["login_map"] = {} - self.configuration.daemon_conf["users"] = [] - self.configuration.daemon_conf["jobs"] = [] - self.configuration.daemon_conf["shares"] = [] - self.configuration.daemon_conf["jupyter_mounts"] = [] - self.configuration.daemon_conf["creds_lock"] = threading.Lock() + self.configuration.daemon_conf['time_stamp'] = 0 + self.configuration.daemon_conf['login_map'] = {} + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['jobs'] = [] + self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf['jupyter_mounts'] = [] + self.configuration.daemon_conf['creds_lock'] = threading.Lock() def test_update_login_map_users(self): """Verify login_map is updated correctly for changed users.""" # Create Login objects user1 = Login( configuration=self.configuration, - username="user1", - home="home1", + username='user1', + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) user2 = Login( configuration=self.configuration, - username="user2", - home="home2", + username='user2', + home='home2', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) # Populate users list - self.configuration.daemon_conf["users"] = [user1, user2] + self.configuration.daemon_conf['users'] = [user1, user2] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, - changed_users=["user1", "user2"], + changed_users=['user1', 'user2'] ) # Verify login_map - login_map = self.configuration.daemon_conf["login_map"] - self.assertIn("user1", login_map) - self.assertIn("user2", login_map) - self.assertEqual(login_map["user1"], [user1]) - self.assertEqual(login_map["user2"], [user2]) + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('user1', login_map) + self.assertIn('user2', login_map) + self.assertEqual(login_map['user1'], [user1]) + self.assertEqual(login_map['user2'], [user2]) def test_update_login_map_jobs(self): """Verify login_map is updated correctly for changed jobs.""" @@ -1976,39 +2287,39 @@ def test_update_login_map_jobs(self): test_job = Login( configuration=self.configuration, username=TEST_JOB_ID, - home="home1", + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, - ip_addr="1.2.3.4", - user_dict=None, + ip_addr='1.2.3.4', + user_dict=None ) other_job = Login( configuration=self.configuration, username=OTHER_JOB_ID, - home="home2", + home='home2', password=None, digest=None, public_key=None, chroot=True, access=None, - ip_addr="5.6.7.8", - user_dict=None, + ip_addr='5.6.7.8', + user_dict=None ) # Populate jobs list - self.configuration.daemon_conf["jobs"] = [test_job, other_job] + self.configuration.daemon_conf['jobs'] = [test_job, other_job] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_jobs=[TEST_JOB_ID, OTHER_JOB_ID], + changed_jobs=[TEST_JOB_ID, OTHER_JOB_ID] ) # Verify login_map - login_map = self.configuration.daemon_conf["login_map"] + login_map = self.configuration.daemon_conf['login_map'] self.assertIn(TEST_JOB_ID, login_map) self.assertIn(OTHER_JOB_ID, login_map) self.assertEqual(login_map[TEST_JOB_ID], [test_job]) @@ -2019,44 +2330,44 @@ def test_update_login_map_shares(self): # Create Login objects share1 = Login( configuration=self.configuration, - username="share1", - home="home1", + username='share1', + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) share2 = Login( configuration=self.configuration, - username="share2", - home="home2", + username='share2', + home='home2', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) # Populate shares list - self.configuration.daemon_conf["shares"] = [share1, share2] + self.configuration.daemon_conf['shares'] = [share1, share2] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_shares=["share1", "share2"], + changed_shares=['share1', 'share2'] ) # Verify login_map - login_map = self.configuration.daemon_conf["login_map"] - self.assertIn("share1", login_map) - self.assertIn("share2", login_map) - self.assertEqual(login_map["share1"], [share1]) - self.assertEqual(login_map["share2"], [share2]) + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('share1', login_map) + self.assertIn('share2', login_map) + self.assertEqual(login_map['share1'], [share1]) + self.assertEqual(login_map['share2'], [share2]) def test_update_login_map_jupyter(self): """Verify login_map is updated correctly for changed jupyter mounts.""" @@ -2064,42 +2375,39 @@ def test_update_login_map_jupyter(self): test_session = Login( configuration=self.configuration, username=TEST_JUPYTER_SESSION_ID, - home="home1", + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) other_session = Login( configuration=self.configuration, username=OTHER_JUPYTER_SESSION_ID, - home="home2", + home='home2', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) # Populate jupyter_mounts list - self.configuration.daemon_conf["jupyter_mounts"] = [ - test_session, - other_session, - ] - + self.configuration.daemon_conf['jupyter_mounts'] = [ + test_session, other_session] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_jupyter=[TEST_JUPYTER_SESSION_ID, OTHER_JUPYTER_SESSION_ID], + changed_jupyter=[TEST_JUPYTER_SESSION_ID, OTHER_JUPYTER_SESSION_ID] ) # Verify login_map - login_map = self.configuration.daemon_conf["login_map"] + login_map = self.configuration.daemon_conf['login_map'] self.assertIn(TEST_JUPYTER_SESSION_ID, login_map) self.assertIn(OTHER_JUPYTER_SESSION_ID, login_map) self.assertEqual(login_map[TEST_JUPYTER_SESSION_ID], [test_session]) @@ -2110,45 +2418,45 @@ def test_update_login_map_nonexistent(self): # Populate users list with one user user1 = Login( configuration=self.configuration, - username="user1", - home="home1", + username='user1', + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) - self.configuration.daemon_conf["users"] = [user1] + self.configuration.daemon_conf['users'] = [user1] # Call the function under test for a non-existent user update_login_map( daemon_conf=self.configuration.daemon_conf, - changed_users=["nonexistent"], + changed_users=['nonexistent'] ) # Verify login_map - login_map = self.configuration.daemon_conf["login_map"] - self.assertIn("nonexistent", login_map) - self.assertEqual(login_map["nonexistent"], []) + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('nonexistent', login_map) + self.assertEqual(login_map['nonexistent'], []) def test_update_login_map_empty_lists(self): """Verify login_map is not changed when changed lists are empty.""" # Populate users list user1 = Login( configuration=self.configuration, - username="user1", - home="home1", + username='user1', + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) - self.configuration.daemon_conf["users"] = [user1] + self.configuration.daemon_conf['users'] = [user1] # Call the function under test with empty lists update_login_map( @@ -2156,11 +2464,11 @@ def test_update_login_map_empty_lists(self): changed_users=[], changed_jobs=[], changed_shares=[], - changed_jupyter=[], + changed_jupyter=[] ) # Verify login_map is unchanged (should be empty because we didn't update for 'user1') - login_map = self.configuration.daemon_conf["login_map"] + login_map = self.configuration.daemon_conf['login_map'] self.assertEqual(len(login_map), 0) def test_update_login_map_lock_handling(self): @@ -2168,27 +2476,28 @@ def test_update_login_map_lock_handling(self): # Create a Login object user1 = Login( configuration=self.configuration, - username="user1", - home="home1", + username='user1', + home='home1', password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None, + user_dict=None ) - self.configuration.daemon_conf["users"] = [user1] + self.configuration.daemon_conf['users'] = [user1] # Call the function under test update_login_map( - daemon_conf=self.configuration.daemon_conf, changed_users=["user1"] + daemon_conf=self.configuration.daemon_conf, + changed_users=['user1'] ) # Verify login_map was updated (lock must have been acquired/released) - login_map = self.configuration.daemon_conf["login_map"] - self.assertIn("user1", login_map) - self.assertEqual(login_map["user1"], [user1]) + login_map = self.configuration.daemon_conf['login_map'] + self.assertIn('user1', login_map) + self.assertEqual(login_map['user1'], [user1]) class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): @@ -2196,48 +2505,40 @@ class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return "testconfig" + return 'testconfig' def before_each(self): """Set up test configuration and reset state before each test.""" # Ensure required directories exist _ensure_dirs_needed_for_userdb(self.configuration) self.configuration.daemon_conf = {} - self.configuration.daemon_conf["users"] = [] - self.configuration.daemon_conf["root_dir"] = ( - self.configuration.user_home - ) - self.configuration.daemon_conf["db_path"] = os.path.join( - self.configuration.user_db_home, "MiG-users.db" - ) - self.configuration.daemon_conf["allow_publickey"] = True - self.configuration.daemon_conf["allow_password"] = True - self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf['users'] = [] + self.configuration.daemon_conf['root_dir'] = self.configuration.user_home + self.configuration.daemon_conf['db_path'] = os.path.join( + self.configuration.user_db_home, "MiG-users.db") + self.configuration.daemon_conf['allow_publickey'] = True + self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf['allow_digest'] = False # Create a test user home self.test_user_home = self._provision_test_user(self, TEST_USER_DN) self.test_user_dir = os.path.basename(self.test_user_home) # Create auth files for the user - self.ssh_auth_paths = _prepare_auth_files(self.test_user_home, ["ssh"]) + self.ssh_auth_paths = _prepare_auth_files(self.test_user_home, ['ssh']) self.ssh_auth_keys_path, self.ssh_auth_pw_path = self.ssh_auth_paths def test_update_user_objects_adds_passwords(self): """Verify that update_user_objects adds Login objects for passwords.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_keys part from auth_keys_path - authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, "") + authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') authkeys = authkeys.lstrip(os.sep) - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') authpw = authpw.lstrip(os.sep) authprotos = (authkeys, authpw, None) - user_tuple = ( - TEST_USER_DN, - TEST_USER_EMAIL, - self.test_user_dir, - TEST_USER_SHORT_ID, - TEST_USER_EMAIL, - ) + user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, + TEST_USER_SHORT_ID, TEST_USER_EMAIL) # Call the helper update_user_objects( configuration=self.configuration, @@ -2245,48 +2546,39 @@ def test_update_user_objects_adds_passwords(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True, + private_auth_file=True ) # Verify that three aliased pw logins were added - pw_logins = [u for u in daemon_conf["users"] if u.password is not None] + pw_logins = [u for u in daemon_conf['users'] if u.password is not None] self.assertEqual(len(pw_logins), 3) for entry in pw_logins: - self.assertIn( - entry.username, - (TEST_USER_DN, TEST_USER_EMAIL, TEST_USER_SHORT_ID), - ) + self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, + TEST_USER_SHORT_ID)) self.assertEqual(entry.home, self.test_user_dir) self.assertEqual(entry.access, READ_WRITE_ACCESS) self.assertEqual(entry.password, TEST_USER_PW_HASH) # Verify that no key logins were added - key_logins = [ - u for u in daemon_conf["users"] if u.public_key is not None - ] + key_logins = [u for u in daemon_conf['users'] + if u.public_key is not None] self.assertEqual(len(key_logins), 0) # Verify that no digest logins were added - digest_logins = [ - u for u in daemon_conf["users"] if u.digest is not None - ] + digest_logins = [u for u in daemon_conf['users'] + if u.digest is not None] self.assertEqual(len(digest_logins), 0) def test_update_user_objects_adds_keys(self): """Verify that update_user_objects adds Login objects for keys.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_keys part from auth_keys_path - authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, "") + authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') authkeys = authkeys.lstrip(os.sep) - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') authpw = authpw.lstrip(os.sep) authprotos = (authkeys, authpw, None) - user_tuple = ( - TEST_USER_DN, - TEST_USER_EMAIL, - self.test_user_dir, - TEST_USER_SHORT_ID, - TEST_USER_EMAIL, - ) + user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, + TEST_USER_SHORT_ID, TEST_USER_EMAIL) # Call the helper update_user_objects( configuration=self.configuration, @@ -2294,20 +2586,17 @@ def test_update_user_objects_adds_keys(self): path=self.ssh_auth_keys_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True, + private_auth_file=True ) # Verify that three aliased key logins were added (dupe keys ignored) - key_logins = [ - u for u in daemon_conf["users"] if u.public_key is not None - ] + key_logins = [u for u in daemon_conf['users'] if u.public_key is not + None] self.assertEqual(len(key_logins), 3) for entry in key_logins: - self.assertIn( - entry.username, - (TEST_USER_DN, TEST_USER_EMAIL, TEST_USER_SHORT_ID), - ) + self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, + TEST_USER_SHORT_ID)) self.assertEqual(entry.home, self.test_user_dir) self.assertEqual(entry.access, READ_WRITE_ACCESS) login_key = entry.public_key @@ -2315,28 +2604,22 @@ def test_update_user_objects_adds_keys(self): self.assertEqual(result, TEST_USER_PUB_KEY) # Verify that no password logins were added - pw_logins = [u for u in daemon_conf["users"] if u.password is not None] + pw_logins = [u for u in daemon_conf['users'] if u.password is not None] self.assertEqual(len(pw_logins), 0) # Verify that no digest logins were added - digest_logins = [ - u for u in daemon_conf["users"] if u.digest is not None - ] + digest_logins = [u for u in daemon_conf['users'] + if u.digest is not None] self.assertEqual(len(digest_logins), 0) def test_update_user_objects_removes_old_entries(self): """Verify that update_user_objects cleans old entries for the same user.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_passwords part from auth_pw_path - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') authpw = authpw.lstrip(os.sep) authprotos = (None, authpw, None) - user_tuple = ( - TEST_USER_DN, - TEST_USER_EMAIL, - self.test_user_dir, - None, - None, - ) + user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, + None, None) # Create a dummy user with a last_update in the past to remove in test past_timestamp = time.time() - 3600 dummy_user = Login( @@ -2344,10 +2627,10 @@ def test_update_user_objects_removes_old_entries(self): username=TEST_USER_EMAIL, home=self.test_user_dir, password=TEST_USER_PW_HASH, - access=READ_WRITE_ACCESS, + access=READ_WRITE_ACCESS ) dummy_user.last_update = past_timestamp - daemon_conf["users"].append(dummy_user) + daemon_conf['users'].append(dummy_user) # Remove saved password os.remove(self.ssh_auth_pw_path) @@ -2358,21 +2641,22 @@ def test_update_user_objects_removes_old_entries(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True, + private_auth_file=True ) # Old entry should be removed - usernames = [u.username for u in daemon_conf["users"]] + usernames = [u.username for u in daemon_conf['users']] self.assertNotIn(TEST_USER_DN, usernames, "Old entry was not removed") def test_update_user_objects_no_changes(self): """Verify that calling update_user_objects with unchanged files does not duplicate entries.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_passwords part from auth_pw_path - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') authpw = authpw.lstrip(os.sep) authprotos = (None, authpw, None) - user_tuple = (TEST_USER_DN, None, self.test_user_dir, None, None) + user_tuple = (TEST_USER_DN, None, self.test_user_dir, + None, None) # First call to populate update_user_objects( configuration=self.configuration, @@ -2380,9 +2664,9 @@ def test_update_user_objects_no_changes(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True, + private_auth_file=True ) - initial_count = len(daemon_conf["users"]) + initial_count = len(daemon_conf['users']) # Second call with same files update_user_objects( @@ -2391,14 +2675,11 @@ def test_update_user_objects_no_changes(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True, - ) - self.assertEqual( - len(daemon_conf["users"]), - initial_count, - "Duplicate entries were added on second call", + private_auth_file=True ) + self.assertEqual(len(daemon_conf['users']), initial_count, + "Duplicate entries were added on second call") -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() From cc765c00d8d3f9c1cdbf6ef0226879a7127c680d Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 15:54:57 +0200 Subject: [PATCH 38/61] Reformat griddaemons unit test suite with make python-format to sync with #575. --- tests/test_mig_shared_griddaemons_login.py | 1587 +++++++++++--------- 1 file changed, 847 insertions(+), 740 deletions(-) diff --git a/tests/test_mig_shared_griddaemons_login.py b/tests/test_mig_shared_griddaemons_login.py index 5d0a4a320..4431cc20e 100644 --- a/tests/test_mig_shared_griddaemons_login.py +++ b/tests/test_mig_shared_griddaemons_login.py @@ -38,12 +38,16 @@ # Imports required for the unit test wrapping from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( - X509_USER_ID_FORMAT, - UUID_USER_ID_FORMAT, READ_WRITE_ACCESS, READ_ONLY_ACCESS, WRITE_ONLY_ACCESS, keyword_auto + READ_ONLY_ACCESS, + READ_WRITE_ACCESS, + UUID_USER_ID_FORMAT, + WRITE_ONLY_ACCESS, + X509_USER_ID_FORMAT, + keyword_auto, ) # Imports of the code under test @@ -67,7 +71,8 @@ # More imports required for the unit test wrapping from mig.shared.useradm import ( - _ensure_dirs_needed_for_userdb, create_user + _ensure_dirs_needed_for_userdb, + create_user ) # Imports required for the unit tests themselves @@ -113,10 +118,12 @@ def _provision_uuid_test_user(configuration, client_id, client_overrides=None): # IMPORTANT: we need to use explicit create_user here for UUID format! user_dict = distinguished_name_to_user(client_id) # NOTE: generate unique and short id based on id to avoid test collisions - user_dict["unique_id"] = binascii.hexlify( - client_id.encode('utf8')).decode('ascii') + user_dict["unique_id"] = binascii.hexlify(client_id.encode("utf8")).decode( + "ascii" + ) user_dict["short_id"] = binascii.hexlify( - user_dict["email"].encode('utf8')).decode('ascii') + user_dict["email"].encode("utf8") + ).decode("ascii") user_dict["comment"] = "This is the user account comment" user_dict["locality"] = "" user_dict["organizational_unit"] = "" @@ -141,22 +148,21 @@ def _prepare_auth_files(home_path, auth_protos=None): """ auth_files = [] if auth_protos is None: - auth_protos = ['davs', 'ftps', 'ssh'] + auth_protos = ["davs", "ftps", "ssh"] # Create requested auth dirs with files for auth in auth_protos: # Create a .PROTO directory with authorized_X file(s) - dot_proto_dir = os.path.join(home_path, '.%s' % auth) + dot_proto_dir = os.path.join(home_path, ".%s" % auth) ensure_dirs_exist(dot_proto_dir) - if auth == 'ssh': - authkeys_path = os.path.join(dot_proto_dir, 'authorized_keys') - with open(authkeys_path, 'w') as creds_fd: + if auth == "ssh": + authkeys_path = os.path.join(dot_proto_dir, "authorized_keys") + with open(authkeys_path, "w") as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) auth_files.append(authkeys_path) - authpasswords_path = os.path.join(dot_proto_dir, - 'authorized_passwords') - with open(authpasswords_path, 'w') as creds_fd: + authpasswords_path = os.path.join(dot_proto_dir, "authorized_passwords") + with open(authpasswords_path, "w") as creds_fd: creds_fd.write(TEST_USER_PW_HASH) auth_files.append(authpasswords_path) return auth_files @@ -173,12 +179,12 @@ def _create_job_mrsl_file(test_case, session_id, job_dict): ensure_dirs_exist(mrsl_dir) # Create actual pickle file in a temp location - pickle_path = temppath('job_%s.pkl' % session_id, test_case) - with open(pickle_path, 'wb') as pkl_fd: + pickle_path = temppath("job_%s.pkl" % session_id, test_case) + with open(pickle_path, "wb") as pkl_fd: pickle.dump(job_dict, pkl_fd) # Create symlink - link_path = os.path.join(mrsl_dir, '%s.mRSL' % session_id) + link_path = os.path.join(mrsl_dir, "%s.mRSL" % session_id) if os.path.lexists(link_path): os.remove(link_path) os.symlink(pickle_path, link_path) @@ -190,11 +196,11 @@ def _create_jupyter_mount_file(test_case, session_id, jupyter_dict): mount_dir = test_case.configuration.sessid_to_jupyter_mount_link_home ensure_dirs_exist(mount_dir) - pickle_path = temppath('jupyter_%s.pkl' % session_id, test_case) - with open(pickle_path, 'wb') as pkl_fd: + pickle_path = temppath("jupyter_%s.pkl" % session_id, test_case) + with open(pickle_path, "wb") as pkl_fd: pickle.dump(jupyter_dict, pkl_fd) - link_path = os.path.join(mount_dir, '%s.jupyter_mount' % session_id) + link_path = os.path.join(mount_dir, "%s.jupyter_mount" % session_id) if os.path.lexists(link_path): os.remove(link_path) os.symlink(pickle_path, link_path) @@ -220,14 +226,14 @@ class MigSharedGriddaemonsLogin__add_job_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test.""" # Initialize daemon_conf with empty jobs list self.configuration.daemon_conf = { - 'jobs': [], - 'creds_lock': threading.Lock() # Ensure lock is present for testing + "jobs": [], + "creds_lock": threading.Lock(), # Ensure lock is present for testing } def test_add_job_object_basic(self): @@ -236,17 +242,17 @@ def test_add_job_object_basic(self): add_job_object( configuration=self.configuration, login=TEST_JOB_ID, - home='home1', + home="home1", password=None, - pubkey=TEST_USER_PUB_KEY + pubkey=TEST_USER_PUB_KEY, ) # Verify the job was added - jobs = self.configuration.daemon_conf['jobs'] + jobs = self.configuration.daemon_conf["jobs"] self.assertEqual(len(jobs), 1) job = jobs[0] self.assertEqual(job.username, TEST_JOB_ID) - self.assertEqual(job.home, 'home1') + self.assertEqual(job.home, "home1") self.assertEqual(job.password, None) # Convert saved paramiko.PKey back to openssh pub key format and check login_key = job.public_key @@ -256,22 +262,22 @@ def test_add_job_object_basic(self): def test_add_job_object_uses_creds_lock(self): """Verify add_job_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_job_object(configuration=self.configuration, - login=OTHER_JOB_ID, - home='job_home2') + self.configuration.daemon_conf["creds_lock"] = fake_lock + add_job_object( + configuration=self.configuration, + login=OTHER_JOB_ID, + home="job_home2", + ) self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) def test_add_job_object_missing_home(self): """Verify that add_job_object uses login as home if home is None.""" add_job_object( - configuration=self.configuration, - login='job3', - home=None + configuration=self.configuration, login="job3", home=None ) - job = self.configuration.daemon_conf['jobs'][0] - self.assertEqual(job.home, 'job3') + job = self.configuration.daemon_conf["jobs"][0] + self.assertEqual(job.home, "job3") class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): @@ -279,14 +285,14 @@ class MigSharedGriddaemonsLogin__add_jupyter_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test.""" # Initialize daemon_conf with empty jupyter_mounts list self.configuration.daemon_conf = { - 'jupyter_mounts': [], - 'creds_lock': threading.Lock() + "jupyter_mounts": [], + "creds_lock": threading.Lock(), } def test_add_jupyter_object_basic(self): @@ -295,16 +301,16 @@ def test_add_jupyter_object_basic(self): add_jupyter_object( configuration=self.configuration, login=TEST_JUPYTER_SESSION_ID, - home='jupyter_home1', - pubkey=TEST_USER_PUB_KEY + home="jupyter_home1", + pubkey=TEST_USER_PUB_KEY, ) # Verify the jupyter mount was added - mounts = self.configuration.daemon_conf['jupyter_mounts'] + mounts = self.configuration.daemon_conf["jupyter_mounts"] self.assertEqual(len(mounts), 1) mount = mounts[0] self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) - self.assertEqual(mount.home, 'jupyter_home1') + self.assertEqual(mount.home, "jupyter_home1") # Convert saved paramiko.PKey back to openssh pub key format and check login_key = mount.public_key result = _parse_pkey_to_openssh_format(login_key) @@ -313,22 +319,22 @@ def test_add_jupyter_object_basic(self): def test_add_jupyter_object_uses_creds_lock(self): """Verify add_jupyter_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_jupyter_object(configuration=self.configuration, - login=OTHER_JUPYTER_SESSION_ID, - home='jupyter_home2') + self.configuration.daemon_conf["creds_lock"] = fake_lock + add_jupyter_object( + configuration=self.configuration, + login=OTHER_JUPYTER_SESSION_ID, + home="jupyter_home2", + ) self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) def test_add_jupyter_object_missing_home(self): """Verify that add_jupyter_object uses login as home if home is None.""" add_jupyter_object( - configuration=self.configuration, - login='jupyter3', - home=None + configuration=self.configuration, login="jupyter3", home=None ) - mount = self.configuration.daemon_conf['jupyter_mounts'][0] - self.assertEqual(mount.home, 'jupyter3') + mount = self.configuration.daemon_conf["jupyter_mounts"][0] + self.assertEqual(mount.home, "jupyter3") class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): @@ -336,33 +342,33 @@ class MigSharedGriddaemonsLogin__add_user_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up daemon_conf for add_user_object tests.""" self.configuration.daemon_conf = { - 'users': [], - 'creds_lock': threading.Lock() + "users": [], + "creds_lock": threading.Lock(), } def test_add_user_object_appends_login(self): """Verify add_user_object appends a Login to users.""" - user_dict = {'user_id': TEST_USER_DN} + user_dict = {"user_id": TEST_USER_DN} add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home='home1', + home="home1", password=TEST_USER_PW_HASH, access=READ_WRITE_ACCESS, - user_dict=user_dict + user_dict=user_dict, ) - self.assertEqual(len(self.configuration.daemon_conf['users']), 1) - login = self.configuration.daemon_conf['users'][0] + self.assertEqual(len(self.configuration.daemon_conf["users"]), 1) + login = self.configuration.daemon_conf["users"][0] self.assertIsInstance(login, Login) self.assertEqual(login.username, TEST_USER_EMAIL) - self.assertEqual(login.home, 'home1') + self.assertEqual(login.home, "home1") self.assertEqual(login.password, TEST_USER_PW_HASH) self.assertIsNone(login.digest) self.assertIsNone(login.public_key) @@ -375,15 +381,17 @@ def test_add_user_object_adds_public_key_login(self): add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home='home1', + home="home1", pubkey=TEST_USER_PUB_KEY, - access=READ_WRITE_ACCESS + access=READ_WRITE_ACCESS, ) - login = self.configuration.daemon_conf['users'][0] + login = self.configuration.daemon_conf["users"][0] self.assertIsNotNone(login.public_key) - key_line = "%s %s" % (login.public_key.get_name(), - login.public_key.get_base64()) + key_line = "%s %s" % ( + login.public_key.get_name(), + login.public_key.get_base64(), + ) self.assertEqual(key_line, TEST_USER_PUB_KEY) def test_add_user_object_adds_digest_login(self): @@ -391,13 +399,13 @@ def test_add_user_object_adds_digest_login(self): add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home='home1', - digest='digest-value', - access=READ_ONLY_ACCESS + home="home1", + digest="digest-value", + access=READ_ONLY_ACCESS, ) - login = self.configuration.daemon_conf['users'][0] - self.assertEqual(login.digest, 'digest-value') + login = self.configuration.daemon_conf["users"][0] + self.assertEqual(login.digest, "digest-value") self.assertIsNone(login.password) self.assertIsNone(login.public_key) self.assertEqual(login.access, READ_ONLY_ACCESS) @@ -405,37 +413,36 @@ def test_add_user_object_adds_digest_login(self): def test_add_user_object_without_lock(self): """Verify add_user_object works when creds_lock is not configured.""" daemon_conf = self.configuration.daemon_conf - daemon_conf.pop('creds_lock') + daemon_conf.pop("creds_lock") add_user_object( configuration=self.configuration, login=TEST_USER_EMAIL, - home='home1' + home="home1", ) - self.assertEqual(len(daemon_conf['users']), 1) - self.assertEqual(daemon_conf['users'][0].username, TEST_USER_EMAIL) + self.assertEqual(len(daemon_conf["users"]), 1) + self.assertEqual(daemon_conf["users"][0].username, TEST_USER_EMAIL) def test_add_user_object_uses_creds_lock(self): """Verify add_user_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_user_object(configuration=self.configuration, - login=TEST_USER_EMAIL, - home='user_home2' - ) + self.configuration.daemon_conf["creds_lock"] = fake_lock + add_user_object( + configuration=self.configuration, + login=TEST_USER_EMAIL, + home="user_home2", + ) self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) def test_add_user_object_uses_login_as_home_when_home_none(self): """Verify add_user_object normalizes missing home to login name.""" add_user_object( - configuration=self.configuration, - login=TEST_USER_EMAIL, - home=None + configuration=self.configuration, login=TEST_USER_EMAIL, home=None ) - login = self.configuration.daemon_conf['users'][0] + login = self.configuration.daemon_conf["users"][0] self.assertEqual(login.home, TEST_USER_EMAIL) @@ -444,19 +451,19 @@ class MigSharedGriddaemonsLogin__add_share_object(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up daemon_conf for add_share_object tests.""" self.configuration.daemon_conf = { - 'shares': [], - 'creds_lock': threading.Lock() + "shares": [], + "creds_lock": threading.Lock(), } def test_add_share_object_appends_login(self): """Verify add_share_object appends a Login to shares.""" user_dict = {} - home_path = temppath('share_home', self) + home_path = temppath("share_home", self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, @@ -464,16 +471,17 @@ def test_add_share_object_appends_login(self): home=home_path, password=TEST_RW_SHARE_ID, access=READ_WRITE_ACCESS, - user_dict=user_dict - ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path, - password=login.password, - access=login.access - ) - self.assertEqual(len(self.configuration.daemon_conf['shares']), 1) - share_login = self.configuration.daemon_conf['shares'][0] + user_dict=user_dict, + ) + add_share_object( + configuration=self.configuration, + login=login.username, + home=home_path, + password=login.password, + access=login.access, + ) + self.assertEqual(len(self.configuration.daemon_conf["shares"]), 1) + share_login = self.configuration.daemon_conf["shares"][0] self.assertEqual(share_login.username, TEST_RW_SHARE_ID) self.assertEqual(share_login.home, home_path) self.assertEqual(share_login.password, TEST_RW_SHARE_ID) @@ -482,47 +490,49 @@ def test_add_share_object_appends_login(self): def test_add_share_object_adds_public_key_login(self): """Verify add_share_object stores parsed public key logins.""" - home_path = temppath('share_home_pubkey', self) + home_path = temppath("share_home_pubkey", self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, username=TEST_RW_SHARE_ID, home=home_path, password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + access=READ_WRITE_ACCESS, ) add_share_object( configuration=self.configuration, login=login, home=home_path, - pubkey=TEST_USER_PUB_KEY + pubkey=TEST_USER_PUB_KEY, ) - share_login = self.configuration.daemon_conf['shares'][0] + share_login = self.configuration.daemon_conf["shares"][0] self.assertIsNotNone(share_login.public_key) - key_line = "%s %s" % (share_login.public_key.get_name(), - share_login.public_key.get_base64()) + key_line = "%s %s" % ( + share_login.public_key.get_name(), + share_login.public_key.get_base64(), + ) self.assertEqual(key_line, TEST_USER_PUB_KEY) def test_add_share_object_adds_digest_login(self): """Verify add_share_object stores digest logins.""" - home_path = temppath('share_home_digest', self) + home_path = temppath("share_home_digest", self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, username=TEST_RO_SHARE_ID, home=home_path, password=TEST_RO_SHARE_ID, - access=READ_ONLY_ACCESS + access=READ_ONLY_ACCESS, ) add_share_object( configuration=self.configuration, login=login, home=home_path, - digest='digest-value', - access=READ_ONLY_ACCESS + digest="digest-value", + access=READ_ONLY_ACCESS, ) - share_login = self.configuration.daemon_conf['shares'][0] - self.assertEqual(share_login.digest, 'digest-value') + share_login = self.configuration.daemon_conf["shares"][0] + self.assertEqual(share_login.digest, "digest-value") self.assertIsNone(share_login.password) self.assertIsNone(share_login.public_key) self.assertEqual(share_login.access, READ_ONLY_ACCESS) @@ -530,29 +540,33 @@ def test_add_share_object_adds_digest_login(self): def test_add_share_object_without_lock(self): """Verify add_share_object works when creds_lock is not configured.""" daemon_conf = self.configuration.daemon_conf - daemon_conf.pop('creds_lock') - home_path = temppath('share_home_nolock', self) + daemon_conf.pop("creds_lock") + home_path = temppath("share_home_nolock", self) ensure_dirs_exist(home_path) login = Login( configuration=self.configuration, username=TEST_RW_SHARE_ID, home=home_path, password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + access=READ_WRITE_ACCESS, ) - add_share_object(configuration=self.configuration, - login=login.username, - home=home_path) - self.assertEqual(len(daemon_conf['shares']), 1) - self.assertEqual(daemon_conf['shares'][0].username, TEST_RW_SHARE_ID) + add_share_object( + configuration=self.configuration, + login=login.username, + home=home_path, + ) + self.assertEqual(len(daemon_conf["shares"]), 1) + self.assertEqual(daemon_conf["shares"][0].username, TEST_RW_SHARE_ID) def test_add_share_object_uses_creds_lock(self): """Verify add_share_object acquires and releases creds_lock.""" fake_lock = _FakeLock() - self.configuration.daemon_conf['creds_lock'] = fake_lock - add_share_object(configuration=self.configuration, - login=TEST_RW_SHARE_ID, - home='share_home2') + self.configuration.daemon_conf["creds_lock"] = fake_lock + add_share_object( + configuration=self.configuration, + login=TEST_RW_SHARE_ID, + home="share_home2", + ) self.assertTrue(fake_lock.acquired) self.assertTrue(fake_lock.released) @@ -563,12 +577,12 @@ def test_add_share_object_uses_login_as_home_when_home_none(self): username=TEST_RW_SHARE_ID, home=None, password=TEST_RW_SHARE_ID, - access=READ_WRITE_ACCESS + access=READ_WRITE_ACCESS, + ) + add_share_object( + configuration=self.configuration, login=login.username, home=None ) - add_share_object(configuration=self.configuration, - login=login.username, - home=None) - share_login = self.configuration.daemon_conf['shares'][0] + share_login = self.configuration.daemon_conf["shares"][0] self.assertEqual(share_login.home, TEST_RW_SHARE_ID) @@ -577,7 +591,7 @@ class MigSharedGriddaemonsLogin__get_creds_changes(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test""" @@ -588,16 +602,16 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["users"] = [] + self.configuration.daemon_conf["allow_publickey"] = True + self.configuration.daemon_conf["allow_password"] = True # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf["allow_digest"] = False self.test_user_home = self._provision_test_user(self, TEST_USER_DN) - auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + auth_files = _prepare_auth_files(self.test_user_home, ["ssh"]) self.auth_keys_path, self.auth_passwords_path = auth_files self.auth_digests_path = None @@ -617,17 +631,17 @@ def test_get_creds_changes_detects_new_files(self): chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) + daemon_conf["users"].append(dummy_user) changed_paths = get_creds_changes( daemon_conf, - 'user', + "user", self.auth_keys_path, self.auth_passwords_path, - self.auth_digests_path + self.auth_digests_path, ) self.assertIn(self.auth_keys_path, changed_paths) @@ -652,17 +666,17 @@ def test_get_creds_changes_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_user.last_update = current_time - daemon_conf['users'].append(dummy_user) + daemon_conf["users"].append(dummy_user) changed_paths = get_creds_changes( daemon_conf, TEST_USER_SHORT_ID, self.auth_keys_path, self.auth_passwords_path, - self.auth_digests_path + self.auth_digests_path, ) self.assertEqual(len(changed_paths), 0) @@ -673,7 +687,7 @@ class MigSharedGriddaemonsLogin__get_creds_changes_uuid_user_id(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test""" @@ -685,28 +699,30 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["users"] = [] + self.configuration.daemon_conf["allow_publickey"] = True + self.configuration.daemon_conf["allow_password"] = True # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf["allow_digest"] = False user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN) - self.test_user_id = user_id = user_dict['unique_id'] + self.test_user_id = user_id = user_dict["unique_id"] client_dir = client_id_dir(TEST_USER_DN) - self.test_user_home = os.path.join(self.configuration.user_home, - user_id) + self.test_user_home = os.path.join( + self.configuration.user_home, user_id + ) self.test_user_dir = os.path.basename(self.test_user_home) - self.test_user_home_x509 = os.path.join(self.configuration.user_home, - client_dir) + self.test_user_home_x509 = os.path.join( + self.configuration.user_home, client_dir + ) self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) # Make sure X509 link are provisioned as well if not os.path.islink(self.test_user_home_x509): os.symlink(self.test_user_home, self.test_user_home_x509) - auth_files = _prepare_auth_files(self.test_user_home, ['ssh']) + auth_files = _prepare_auth_files(self.test_user_home, ["ssh"]) self.auth_keys_path, self.auth_passwords_path = auth_files self.auth_digests_path = None @@ -727,17 +743,17 @@ def test_get_creds_changes_detects_new_files(self): chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) + daemon_conf["users"].append(dummy_user) changed_paths = get_creds_changes( daemon_conf, - 'user', + "user", self.auth_keys_path, self.auth_passwords_path, - self.auth_digests_path + self.auth_digests_path, ) self.assertIn(self.auth_keys_path, changed_paths) @@ -763,17 +779,17 @@ def test_get_creds_changes_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_user.last_update = current_time - daemon_conf['users'].append(dummy_user) + daemon_conf["users"].append(dummy_user) changed_paths = get_creds_changes( daemon_conf, TEST_USER_SHORT_ID, self.auth_keys_path, self.auth_passwords_path, - self.auth_digests_path + self.auth_digests_path, ) self.assertEqual(len(changed_paths), 0) @@ -784,7 +800,7 @@ class MigSharedGriddaemonsLogin__get_job_changes(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test""" @@ -792,22 +808,22 @@ def before_each(self): ensure_dirs_exist(self.configuration.sharelink_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['jobs'] = [] + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["jobs"] = [] # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf["allow_digest"] = False - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) + self.auth_keys_path = temppath("authorized_keys", self) + self.auth_passwords_path = temppath("authorized_passwords", self) # self.auth_digests_path = temppath('authhorized_digests', self) self.auth_digests_path = None # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: + with open(self.auth_keys_path, "w") as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: + with open(self.auth_passwords_path, "w") as creds_fd: creds_fd.write(TEST_USER_PW_HASH) # with open(self.auth_digests_path, 'w') as creds_fd: # creds_fd.write(TEST_USER_DIGEST) @@ -816,14 +832,12 @@ def test_get_job_changes_new_job(self): """Verify that a new job mrsl file is detected as a change""" daemon_conf = self.configuration.daemon_conf - mrsl_path = temppath('test_job.mRSL', self) - with open(mrsl_path, 'w') as mrsl_fd: - mrsl_fd.write('test content') + mrsl_path = temppath("test_job.mRSL", self) + with open(mrsl_path, "w") as mrsl_fd: + mrsl_fd.write("test content") changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path + daemon_conf, "test_session_id", mrsl_path ) self.assertIn(mrsl_path, changed_paths) @@ -832,31 +846,29 @@ def test_get_job_changes_no_changes(self): """Verify that unchanged job mrsl file returns an empty list""" daemon_conf = self.configuration.daemon_conf - mrsl_path = temppath('test_job.mRSL', self) - with open(mrsl_path, 'w') as mrsl_fd: - mrsl_fd.write('test content') + mrsl_path = temppath("test_job.mRSL", self) + with open(mrsl_path, "w") as mrsl_fd: + mrsl_fd.write("test content") # Create a dummy job with last_update matching file mtime current_time = time.time() dummy_job = Login( configuration=self.configuration, - username='test_session_id', - home='job_home', + username="test_session_id", + home="job_home", password=None, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_job.last_update = current_time - daemon_conf['jobs'].append(dummy_job) + daemon_conf["jobs"].append(dummy_job) changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path + daemon_conf, "test_session_id", mrsl_path ) self.assertEqual(len(changed_paths), 0) @@ -868,24 +880,22 @@ def test_get_job_changes_missing_file(self): # Create a dummy job dummy_job = Login( configuration=self.configuration, - username='test_session_id', - home='job_home', + username="test_session_id", + home="job_home", password=None, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) - daemon_conf['jobs'].append(dummy_job) + daemon_conf["jobs"].append(dummy_job) - mrsl_path = temppath('missing_job.mRSL', self) + mrsl_path = temppath("missing_job.mRSL", self) changed_paths = get_job_changes( - daemon_conf, - 'test_session_id', - mrsl_path + daemon_conf, "test_session_id", mrsl_path ) self.assertIn(mrsl_path, changed_paths) @@ -896,38 +906,41 @@ class MigSharedGriddaemonsLogin__get_share_changes(MigTestCase): def _provide_configuration(self): """Return a test configuration instance""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test""" _ensure_dirs_needed_for_userdb(self.configuration) - self.ro_share_home = os.path.join(self.configuration.sharelink_home, - 'read-only') - self.rw_share_home = os.path.join(self.configuration.sharelink_home, - 'read-write') - self.wo_share_home = os.path.join(self.configuration.sharelink_home, - 'write-only') + self.ro_share_home = os.path.join( + self.configuration.sharelink_home, "read-only" + ) + self.rw_share_home = os.path.join( + self.configuration.sharelink_home, "read-write" + ) + self.wo_share_home = os.path.join( + self.configuration.sharelink_home, "write-only" + ) ensure_dirs_exist(self.ro_share_home) ensure_dirs_exist(self.rw_share_home) ensure_dirs_exist(self.wo_share_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['shares'] = [] + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["shares"] = [] # TODO: enable and test unsafe digest auth, too? # self.configuration.daemon_conf['allow_digest'] = True - self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf["allow_digest"] = False - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) + self.auth_keys_path = temppath("authorized_keys", self) + self.auth_passwords_path = temppath("authorized_passwords", self) # self.auth_digests_path = temppath('authhorized_digests', self) self.auth_digests_path = None # Create sample credential files - with open(self.auth_keys_path, 'w') as creds_fd: + with open(self.auth_keys_path, "w") as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: + with open(self.auth_passwords_path, "w") as creds_fd: creds_fd.write(TEST_USER_PW_HASH) # with open(self.auth_digests_path, 'w') as creds_fd: # creds_fd.write(TEST_USER_DIGEST) @@ -935,13 +948,15 @@ def before_each(self): def test_get_share_changes_detects_updates(self): """Verify that share link and key file changes are detected""" daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True + daemon_conf["allow_publickey"] = True - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') + user_shared_dir = os.path.join( + self.configuration.user_home, "TestUser", "shared", "data" + ) ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join(user_shared_dir, '.ssh', - 'authorized_keys') + user_shared_keys = os.path.join( + user_shared_dir, ".ssh", "authorized_keys" + ) share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) @@ -950,23 +965,20 @@ def test_get_share_changes_detects_updates(self): dummy_share = Login( configuration=self.configuration, username=TEST_RO_SHARE_ID, - home='share_home', + home="share_home", password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_share.last_update = past_timestamp - daemon_conf['shares'].append(dummy_share) + daemon_conf["shares"].append(dummy_share) changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - user_shared_keys + daemon_conf, TEST_RO_SHARE_ID, share_link_path, user_shared_keys ) self.assertIn(share_link_path, changed_paths) @@ -975,19 +987,17 @@ def test_get_share_changes_detects_updates(self): def test_get_share_changes_new_share(self): """Verify that a new share link is detected as a change""" daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True + daemon_conf["allow_publickey"] = True - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') + user_shared_dir = os.path.join( + self.configuration.user_home, "TestUser", "shared", "data" + ) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - self.auth_keys_path + daemon_conf, TEST_RO_SHARE_ID, share_link_path, self.auth_keys_path ) self.assertIn(share_link_path, changed_paths) @@ -996,13 +1006,15 @@ def test_get_share_changes_new_share(self): def test_get_share_changes_no_changes(self): """Verify that unchanged share files return an empty list""" daemon_conf = self.configuration.daemon_conf - daemon_conf['allow_publickey'] = True + daemon_conf["allow_publickey"] = True - user_shared_dir = os.path.join(self.configuration.user_home, - 'TestUser', 'shared', 'data') + user_shared_dir = os.path.join( + self.configuration.user_home, "TestUser", "shared", "data" + ) ensure_dirs_exist(user_shared_dir) - user_shared_keys = os.path.join(user_shared_dir, '.ssh', - 'authorized_keys') + user_shared_keys = os.path.join( + user_shared_dir, ".ssh", "authorized_keys" + ) ensure_dirs_exist(user_shared_keys) share_link_path = os.path.join(self.ro_share_home, TEST_RO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) @@ -1012,23 +1024,20 @@ def test_get_share_changes_no_changes(self): dummy_share = Login( configuration=self.configuration, username=TEST_RO_SHARE_ID, - home='share_home', + home="share_home", password=TEST_USER_PW_HASH, digest=None, public_key=TEST_USER_PUB_KEY, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) dummy_share.last_update = current_time - daemon_conf['shares'].append(dummy_share) + daemon_conf["shares"].append(dummy_share) changed_paths = get_share_changes( - daemon_conf, - TEST_RO_SHARE_ID, - share_link_path, - user_shared_keys + daemon_conf, TEST_RO_SHARE_ID, share_link_path, user_shared_keys ) self.assertEqual(len(changed_paths), 0) @@ -1039,48 +1048,48 @@ class MigSharedGriddaemonsLogin__login_map_lookup(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration with login_map and creds_lock.""" # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['login_map'] = {} - self.configuration.daemon_conf['creds_lock'] = threading.Lock() + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["login_map"] = {} + self.configuration.daemon_conf["creds_lock"] = threading.Lock() def test_user_exists_with_multiple_credentials(self): """Verify login_map_lookup returns all credentials for a user.""" # Create two Login objects for 'user1' cred1 = Login( configuration=self.configuration.daemon_conf, - username='user1', - home='home1', + username="user1", + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) cred2 = Login( configuration=self.configuration, - username='user1', - home='home2', + username="user1", + home="home2", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) # Populate login_map daemon_conf = self.configuration.daemon_conf - daemon_conf['login_map']['user1'] = [cred1, cred2] + daemon_conf["login_map"]["user1"] = [cred1, cred2] # Call the function - result = login_map_lookup(daemon_conf, 'user1') + result = login_map_lookup(daemon_conf, "user1") # Assert self.assertEqual(result, [cred1, cred2]) @@ -1088,25 +1097,25 @@ def test_user_exists_with_single_credential(self): """Verify login_map_lookup returns one credential for a user.""" cred = Login( configuration=self.configuration, - username='user2', - home='home3', + username="user2", + home="home3", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) daemon_conf = self.configuration.daemon_conf - daemon_conf['login_map']['user2'] = [cred] - result = login_map_lookup(daemon_conf, 'user2') + daemon_conf["login_map"]["user2"] = [cred] + result = login_map_lookup(daemon_conf, "user2") self.assertEqual(result, [cred]) def test_user_does_not_exist(self): """Verify login_map_lookup returns empty list for non-existent user.""" daemon_conf = self.configuration.daemon_conf - result = login_map_lookup(daemon_conf, 'nosuchuser') + result = login_map_lookup(daemon_conf, "nosuchuser") self.assertEqual(result, []) def test_lock_handling(self): @@ -1116,27 +1125,27 @@ def test_lock_handling(self): daemon_conf = self.configuration.daemon_conf cred = Login( configuration=self.configuration, - username='user3', - home='home4', + username="user3", + home="home4", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) - daemon_conf['login_map']['user3'] = [cred] + daemon_conf["login_map"]["user3"] = [cred] # Call the function (lock should be acquired/released internally) - result = login_map_lookup(daemon_conf, 'user3') - self.assertEqual(result, [daemon_conf['login_map']['user3'][0]]) + result = login_map_lookup(daemon_conf, "user3") + self.assertEqual(result, [daemon_conf["login_map"]["user3"][0]]) class MigSharedGriddaemonsLogin__refresh_job_creds(MigTestCase): """Unit tests for the refresh_job_creds helper.""" def _provide_configuration(self): - return 'testconfig' + return "testconfig" def before_each(self): """Set up configuration and directories.""" @@ -1146,92 +1155,104 @@ def before_each(self): ensure_dirs_exist(self.configuration.mrsl_files_dir) ensure_dirs_exist(self.configuration.sessid_to_mrsl_link_home) self.configuration.daemon_conf = { - 'jobs': [], - 'time_stamp': 0, - 'creds_lock': threading.Lock(), - 'allow_publickey': True, + "jobs": [], + "time_stamp": 0, + "creds_lock": threading.Lock(), + "allow_publickey": True, } # Ensure the user dir for the job user exists (for chroot validation) - self.job_user_home = os.path.join(self.configuration.user_home, - TEST_JOB_USER_DIR) + self.job_user_home = os.path.join( + self.configuration.user_home, TEST_JOB_USER_DIR + ) ensure_dirs_exist(self.job_user_home) def test_refresh_job_creds_invalid_protocol(self): """Invalid protocol should return early with no changes.""" - conf, changed = refresh_job_creds(self.configuration, 'invalid', - TEST_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "invalid", TEST_JOB_ID + ) self.assertEqual(changed, []) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) def test_refresh_job_creds_invalid_job_id(self): """Invalid job ID format should return early.""" # possible_job_id usually checks for alphanumeric + dash/underscore - conf, changed = refresh_job_creds(self.configuration, 'sftp', - 'invalid@id') + conf, changed = refresh_job_creds( + self.configuration, "sftp", "invalid@id" + ) self.assertEqual(changed, []) def test_refresh_job_creds_no_link(self): """Missing symlink should report change (removal) if job existed, else nothing.""" # Case 1: Job does not exist in conf, link missing -> no change conf, changed = refresh_job_creds( - self.configuration, 'sftp', OTHER_JOB_ID) + self.configuration, "sftp", OTHER_JOB_ID + ) self.assertEqual(changed, []) # Case 2: Job exists in conf, link missing -> removal - add_job_object(self.configuration, OTHER_JOB_ID, 'home', - pubkey=TEST_USER_PUB_KEY) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - OTHER_JOB_ID) + add_job_object( + self.configuration, OTHER_JOB_ID, "home", pubkey=TEST_USER_PUB_KEY + ) + conf, changed = refresh_job_creds( + self.configuration, "sftp", OTHER_JOB_ID + ) self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) def test_refresh_job_creds_link_unchanged(self): """Unchanged link (mtime <= time_stamp) should return no changes.""" job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': 'staticjob', - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + "STATUS": "EXECUTING", + "SESSIONID": "staticjob", + "USER_CERT": TEST_JOB_USER_DN, + "MOUNT": "mount", + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, } - _create_job_mrsl_file(self, 'staticjob', job_dict) + _create_job_mrsl_file(self, "staticjob", job_dict) # Pre-populate conf with job having current timestamp current_time = time.time() - self.configuration.daemon_conf['time_stamp'] = current_time - add_job_object(self.configuration, 'staticjob', TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY) + self.configuration.daemon_conf["time_stamp"] = current_time + add_job_object( + self.configuration, + "staticjob", + TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY, + ) # Manually set last_update to now to simulate "fresh" - self.configuration.daemon_conf['jobs'][0].last_update = current_time + self.configuration.daemon_conf["jobs"][0].last_update = current_time conf, changed = refresh_job_creds( - self.configuration, 'sftp', 'staticjob') + self.configuration, "sftp", "staticjob" + ) self.assertEqual(changed, []) - self.assertEqual(len(conf['jobs']), 1) + self.assertEqual(len(conf["jobs"]), 1) def test_refresh_job_creds_valid_job_added(self): """Valid executing job with key should be added to jobs.""" job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': TEST_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + "STATUS": "EXECUTING", + "SESSIONID": TEST_JOB_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNT": "mount", + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, } _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", TEST_JOB_ID + ) self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(len(conf['jobs']), 1) - job = conf['jobs'][0] + self.assertEqual(len(conf["jobs"]), 1) + job = conf["jobs"][0] self.assertEqual(job.username, TEST_JOB_ID) self.assertEqual(job.home, TEST_JOB_USER_DIR) - self.assertEqual(job.ip_addr, '127.0.0.1') + self.assertEqual(job.ip_addr, "127.0.0.1") self.assertIsNotNone(job.public_key) # Convert saved paramiko.PKey back to openssh pub key format and check login_key = job.public_key @@ -1243,116 +1264,127 @@ def test_refresh_job_creds_non_executing_status(self): daemon_conf = self.configuration.daemon_conf # Directly register test job as executing before test job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': TEST_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + "STATUS": "EXECUTING", + "SESSIONID": TEST_JOB_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNT": "mount", + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, } _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) # Pre-add job to active jobs in conf jobs list - add_job_object(self.configuration, TEST_JOB_ID, TEST_JOB_USER_DIR, - pubkey=TEST_USER_PUB_KEY) + add_job_object( + self.configuration, + TEST_JOB_ID, + TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY, + ) # NOTE: double check that job is in place for test - self.assertEqual(len(daemon_conf['jobs']), 1) - self.assertIn(TEST_JOB_ID, [i.username for i in daemon_conf['jobs']]) + self.assertEqual(len(daemon_conf["jobs"]), 1) + self.assertIn(TEST_JOB_ID, [i.username for i in daemon_conf["jobs"]]) # Now mark finished and test that refresh removes it - job_dict['STATUS'] = 'FINISHED' + job_dict["STATUS"] = "FINISHED" _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", TEST_JOB_ID + ) # NOTE: job is detected changed and should be removed self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) def test_refresh_job_creds_full_execution_cycle(self): """Job going through states should be added and removed when done.""" job_dict = { - 'STATUS': 'QUEUED', - 'SESSIONID': TEST_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + "STATUS": "QUEUED", + "SESSIONID": TEST_JOB_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNT": "mount", + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, } _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) # Verify that still only queued jobs don't get added - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", TEST_JOB_ID + ) # NOTE: job is detected changed but should remain inactive self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) # Verify that job gets added when changing to executing - job_dict['STATUS'] = 'EXECUTING' + job_dict["STATUS"] = "EXECUTING" _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", TEST_JOB_ID + ) # NOTE: job is detected changed and should be inserted self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(len(conf['jobs']), 1) - self.assertEqual(conf['jobs'][0].username, TEST_JOB_ID) + self.assertEqual(len(conf["jobs"]), 1) + self.assertEqual(conf["jobs"][0].username, TEST_JOB_ID) # Verify that jobs get removed again when finished - job_dict['STATUS'] = 'FINISHED' + job_dict["STATUS"] = "FINISHED" _create_job_mrsl_file(self, TEST_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - TEST_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", TEST_JOB_ID + ) # NOTE: job is detected changed and should be removed self.assertIn(TEST_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) def test_refresh_job_creds_broken_key(self): """Job with unparsable public key should be treated as inactive.""" job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': OTHER_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': 'invalid-key-data', - 'RESOURCE_CONFIG': {'HOSTURL': 'localhost'}, + "STATUS": "EXECUTING", + "SESSIONID": OTHER_JOB_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNT": "mount", + "MOUNTSSHPUBLICKEY": "invalid-key-data", + "RESOURCE_CONFIG": {"HOSTURL": "localhost"}, } _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) - conf, changed = refresh_job_creds(self.configuration, 'sftp', - OTHER_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", OTHER_JOB_ID + ) self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) def test_refresh_job_creds_unresolvable_host(self): """Job with unresolvable HOSTURL should be treated as inactive.""" job_dict = { - 'STATUS': 'EXECUTING', - 'SESSIONID': OTHER_JOB_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNT': 'mount', - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, - 'RESOURCE_CONFIG': {'HOSTURL': 'nonexistent.invalid.tld'}, + "STATUS": "EXECUTING", + "SESSIONID": OTHER_JOB_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNT": "mount", + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, + "RESOURCE_CONFIG": {"HOSTURL": "nonexistent.invalid.tld"}, } _create_job_mrsl_file(self, OTHER_JOB_ID, job_dict) # Mock socket to raise gaierror original_gethostbyname_ex = socket.gethostbyname_ex - socket.gethostbyname_ex = lambda h: ( - _ for _ in ()).throw(socket.gaierror) + socket.gethostbyname_ex = lambda h: (_ for _ in ()).throw( + socket.gaierror + ) try: - conf, changed = refresh_job_creds(self.configuration, 'sftp', - OTHER_JOB_ID) + conf, changed = refresh_job_creds( + self.configuration, "sftp", OTHER_JOB_ID + ) finally: socket.gethostbyname_ex = original_gethostbyname_ex self.assertIn(OTHER_JOB_ID, changed) - self.assertEqual(conf['jobs'], []) + self.assertEqual(conf["jobs"], []) class MigSharedGriddaemonsLogin__refresh_jupyter_creds(MigTestCase): """Unit tests for the refresh_jupyter_creds helper.""" def _provide_configuration(self): - return 'testconfig' + return "testconfig" def before_each(self): """Set up configuration and directories.""" @@ -1362,26 +1394,29 @@ def before_each(self): ensure_dirs_exist(self.configuration.jupyter_mount_files_dir) ensure_dirs_exist(self.configuration.sessid_to_jupyter_mount_link_home) self.configuration.daemon_conf = { - 'jupyter_mounts': [], - 'time_stamp': 0, - 'creds_lock': threading.Lock(), - 'allow_publickey': True, + "jupyter_mounts": [], + "time_stamp": 0, + "creds_lock": threading.Lock(), + "allow_publickey": True, } - self.jupyter_user_home = os.path.join(self.configuration.user_home, - TEST_JOB_USER_DIR) + self.jupyter_user_home = os.path.join( + self.configuration.user_home, TEST_JOB_USER_DIR + ) ensure_dirs_exist(self.jupyter_user_home) def test_refresh_jupyter_creds_invalid_protocol(self): """Invalid protocol should return early.""" - conf, changed = refresh_jupyter_creds(self.configuration, 'invalid', - TEST_JUPYTER_SESSION_ID) + conf, changed = refresh_jupyter_creds( + self.configuration, "invalid", TEST_JUPYTER_SESSION_ID + ) self.assertEqual(changed, []) def test_refresh_jupyter_creds_invalid_id(self): """Invalid jupyter mount ID format should return early.""" - conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', - 'invalid@id') + conf, changed = refresh_jupyter_creds( + self.configuration, "sftp", "invalid@id" + ) self.assertEqual(changed, []) @unittest.skip("TODO: implement the mentioned clean up and enable next") @@ -1389,37 +1424,45 @@ def test_refresh_jupyter_creds_no_link(self): """Missing symlink should clean up existing mounts for that user_dir.""" daemon_conf = self.configuration.daemon_conf # Directly add a stale mount - add_jupyter_object(self.configuration, TEST_JUPYTER_SESSION_ID, - TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) + add_jupyter_object( + self.configuration, + TEST_JUPYTER_SESSION_ID, + TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY, + ) # NOTE: double check that jupyter session is in place for test - self.assertEqual(len(daemon_conf['jupyter_mounts']), 1) - self.assertIn(TEST_JUPYTER_SESSION_ID, - [i.username for i in daemon_conf['jupyter_mounts']]) + self.assertEqual(len(daemon_conf["jupyter_mounts"]), 1) + self.assertIn( + TEST_JUPYTER_SESSION_ID, + [i.username for i in daemon_conf["jupyter_mounts"]], + ) # Now test that refresh removes it - conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', - TEST_JUPYTER_SESSION_ID) + conf, changed = refresh_jupyter_creds( + self.configuration, "sftp", TEST_JUPYTER_SESSION_ID + ) # NOTE: mount is detected unchanged and mount should be removed self.assertEqual(len(changed), 0) - self.assertEqual(conf['jupyter_mounts'], []) + self.assertEqual(conf["jupyter_mounts"], []) def test_refresh_jupyter_creds_valid_mount_added(self): """Valid jupyter mount should be added.""" jupyter_dict = { - 'SESSIONID': TEST_JUPYTER_SESSION_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + "SESSIONID": TEST_JUPYTER_SESSION_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, } _create_jupyter_mount_file(self, TEST_JUPYTER_SESSION_ID, jupyter_dict) - conf, changed = refresh_jupyter_creds(self.configuration, 'sftp', - TEST_JUPYTER_SESSION_ID) + conf, changed = refresh_jupyter_creds( + self.configuration, "sftp", TEST_JUPYTER_SESSION_ID + ) self.assertIn(TEST_JUPYTER_SESSION_ID, changed) - self.assertEqual(len(conf['jupyter_mounts']), 1) - mount = conf['jupyter_mounts'][0] + self.assertEqual(len(conf["jupyter_mounts"]), 1) + mount = conf["jupyter_mounts"][0] self.assertEqual(mount.username, TEST_JUPYTER_SESSION_ID) self.assertEqual(mount.home, TEST_JOB_USER_DIR) self.assertIsNotNone(mount.public_key) @@ -1431,54 +1474,59 @@ def test_refresh_jupyter_creds_valid_mount_added(self): def test_refresh_jupyter_creds_purges_old_keys_same_home(self): """Adding a new mount for same home should purge old mounts.""" # Add stale mount with different session ID but same home - add_jupyter_object(self.configuration, OTHER_JUPYTER_SESSION_ID, - TEST_JOB_USER_DIR, pubkey=TEST_USER_PUB_KEY) + add_jupyter_object( + self.configuration, + OTHER_JUPYTER_SESSION_ID, + TEST_JOB_USER_DIR, + pubkey=TEST_USER_PUB_KEY, + ) jupyter_dict = { - 'SESSIONID': OTHER_JUPYTER_SESSION_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNTSSHPUBLICKEY': TEST_USER_PUB_KEY, + "SESSIONID": OTHER_JUPYTER_SESSION_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNTSSHPUBLICKEY": TEST_USER_PUB_KEY, } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, - jupyter_dict) + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) conf, changed = refresh_jupyter_creds( - self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) + self.configuration, "sftp", OTHER_JUPYTER_SESSION_ID + ) self.assertIn(OTHER_JUPYTER_SESSION_ID, changed) # Only the new one should remain - self.assertEqual(len(conf['jupyter_mounts']), 1) - self.assertEqual(conf['jupyter_mounts'][0].username, - OTHER_JUPYTER_SESSION_ID) + self.assertEqual(len(conf["jupyter_mounts"]), 1) + self.assertEqual( + conf["jupyter_mounts"][0].username, OTHER_JUPYTER_SESSION_ID + ) def test_refresh_jupyter_creds_broken_key(self): """Unparsable key should result in no mount added.""" jupyter_dict = { - 'SESSIONID': OTHER_JUPYTER_SESSION_ID, - 'USER_CERT': TEST_JOB_USER_DN, - 'MOUNTSSHPUBLICKEY': 'invalid-key', + "SESSIONID": OTHER_JUPYTER_SESSION_ID, + "USER_CERT": TEST_JOB_USER_DN, + "MOUNTSSHPUBLICKEY": "invalid-key", } - _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, - jupyter_dict) + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) conf, changed = refresh_jupyter_creds( - self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) + self.configuration, "sftp", OTHER_JUPYTER_SESSION_ID + ) self.assertEqual(changed, []) - self.assertEqual(conf['jupyter_mounts'], []) + self.assertEqual(conf["jupyter_mounts"], []) def test_refresh_jupyter_creds_missing_fields(self): """Missing required fields in dict should result in no mount.""" jupyter_dict = { - 'SESSIONID': OTHER_JUPYTER_SESSION_ID, + "SESSIONID": OTHER_JUPYTER_SESSION_ID, # Missing USER_CERT and MOUNTSSHPUBLICKEY } - _create_jupyter_mount_file( - self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) + _create_jupyter_mount_file(self, OTHER_JUPYTER_SESSION_ID, jupyter_dict) conf, changed = refresh_jupyter_creds( - self.configuration, 'sftp', OTHER_JUPYTER_SESSION_ID) + self.configuration, "sftp", OTHER_JUPYTER_SESSION_ID + ) self.assertEqual(changed, []) - self.assertEqual(conf['jupyter_mounts'], []) + self.assertEqual(conf["jupyter_mounts"], []) class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): @@ -1486,46 +1534,50 @@ class MigSharedGriddaemonsLogin__refresh_share_creds(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test.""" # The base class already creates the required directory layout. # Ensure the share‑link home exists – it is used by refresh_share_creds. - self.ro_share_home = os.path.join(self.configuration.sharelink_home, - 'read-only') - self.rw_share_home = os.path.join(self.configuration.sharelink_home, - 'read-write') - self.wo_share_home = os.path.join(self.configuration.sharelink_home, - 'write-only') + self.ro_share_home = os.path.join( + self.configuration.sharelink_home, "read-only" + ) + self.rw_share_home = os.path.join( + self.configuration.sharelink_home, "read-write" + ) + self.wo_share_home = os.path.join( + self.configuration.sharelink_home, "write-only" + ) ensure_dirs_exist(self.ro_share_home) ensure_dirs_exist(self.rw_share_home) ensure_dirs_exist(self.wo_share_home) self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['shares'] = [] - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["shares"] = [] + self.configuration.daemon_conf["allow_publickey"] = True + self.configuration.daemon_conf["allow_password"] = True + self.configuration.daemon_conf["allow_digest"] = False # Paths that the function will look at - self.auth_keys_path = temppath('authorized_keys', self) - self.auth_passwords_path = temppath('authorized_passwords', self) + self.auth_keys_path = temppath("authorized_keys", self) + self.auth_passwords_path = temppath("authorized_passwords", self) self.auth_digests_path = None # Create dummy credential files - with open(self.auth_keys_path, 'w') as creds_fd: + with open(self.auth_keys_path, "w") as creds_fd: creds_fd.write(TEST_USER_PUB_KEY) - with open(self.auth_passwords_path, 'w') as creds_fd: + with open(self.auth_passwords_path, "w") as creds_fd: creds_fd.write(TEST_USER_PW_HASH) def test_refresh_share_creds_adds_new_share(self): """A new share link should be added to daemon_conf['shares'].""" # Build a share link that points to a temporary user directory - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) + rel_share_home = os.path.join("TestUser", "shared", "data") + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home + ) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) @@ -1533,11 +1585,11 @@ def test_refresh_share_creds_adds_new_share(self): # Call the function under test # NOTE: only sftp access is supported for now - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) # The share should now be present in the changed list @@ -1545,7 +1597,8 @@ def test_refresh_share_creds_adds_new_share(self): # Verify that a Login object was added to shares share_login = [ - obj for obj in updated_conf['shares'] + obj + for obj in updated_conf["shares"] if obj.username == TEST_RW_SHARE_ID ] self.assertEqual(len(share_login), 1) @@ -1561,23 +1614,24 @@ def test_refresh_share_creds_adds_new_share(self): def test_refresh_share_creds_adds_new_share_with_key(self): """A new share link with key should be added twice to daemon_conf['shares'].""" # Build a share link that points to a temporary user directory - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) + rel_share_home = os.path.join("TestUser", "shared", "data") + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home + ) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) os.symlink(user_shared_dir, share_link_path) - _prepare_auth_files(user_shared_dir, ['ssh']) + _prepare_auth_files(user_shared_dir, ["ssh"]) # Call the function under test # NOTE: only sftp access is supported for now - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) # The share should now be present twice in the changed list @@ -1585,7 +1639,8 @@ def test_refresh_share_creds_adds_new_share_with_key(self): # Verify that a Login object was added to shares share_login = [ - obj for obj in updated_conf['shares'] + obj + for obj in updated_conf["shares"] if obj.username == TEST_RW_SHARE_ID ] self.assertEqual(len(share_login), 2) @@ -1610,9 +1665,10 @@ def test_refresh_share_creds_no_changes(self): daemon_conf = self.configuration.daemon_conf # Create a share link that already exists - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) + rel_share_home = os.path.join("TestUser", "shared", "data") + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home + ) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) @@ -1631,76 +1687,87 @@ def test_refresh_share_creds_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None) + user_dict=None, + ) dummy_share.last_update = current_time - daemon_conf['shares'].append(dummy_share) + daemon_conf["shares"].append(dummy_share) # Run the function - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) # No changes should be reported self.assertEqual(len(changed_shares), 0) # The dummy entry should still be the only one present - self.assertEqual(len(updated_conf['shares']), 1) + self.assertEqual(len(updated_conf["shares"]), 1) def test_refresh_share_creds_adds_readonly_share(self): """Test that a read‑only share is added correctly""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) + rel_share_home = os.path.join("TestUser", "shared", "data") + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home + ) ensure_dirs_exist(user_shared_dir) - share_link_path = os.path.join(self.configuration.sharelink_home, - 'read-only', TEST_RO_SHARE_ID) + share_link_path = os.path.join( + self.configuration.sharelink_home, "read-only", TEST_RO_SHARE_ID + ) os.symlink(user_shared_dir, share_link_path) - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RO_SHARE_ID, - share_modes=(READ_ONLY_ACCESS, ) + share_modes=(READ_ONLY_ACCESS,), ) self.assertIn(TEST_RO_SHARE_ID, changed_shares) - share_login = [obj for obj in updated_conf['shares'] - if obj.username == TEST_RO_SHARE_ID] + share_login = [ + obj + for obj in updated_conf["shares"] + if obj.username == TEST_RO_SHARE_ID + ] self.assertEqual(len(share_login), 1) self.assertEqual(share_login[0].home, rel_share_home) def test_refresh_share_creds_adds_writeonly_share(self): """Test that a write‑only share is added correctly""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') - user_shared_dir = os.path.join(self.configuration.user_home, - rel_share_home) + rel_share_home = os.path.join("TestUser", "shared", "data") + user_shared_dir = os.path.join( + self.configuration.user_home, rel_share_home + ) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.wo_share_home, TEST_WO_SHARE_ID) os.symlink(user_shared_dir, share_link_path) - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_WO_SHARE_ID, - share_modes=(WRITE_ONLY_ACCESS, ) + share_modes=(WRITE_ONLY_ACCESS,), ) self.assertIn(TEST_WO_SHARE_ID, changed_shares) - share_login = [obj for obj in updated_conf['shares'] - if obj.username == TEST_WO_SHARE_ID] + share_login = [ + obj + for obj in updated_conf["shares"] + if obj.username == TEST_WO_SHARE_ID + ] self.assertEqual(len(share_login), 1) self.assertEqual(share_login[0].home, rel_share_home) def test_refresh_share_creds_no_change_on_unchanged_link(self): """Test that an unchanged share link does not trigger a change""" - rel_share_home = os.path.join('TestUser', 'shared', 'data') + rel_share_home = os.path.join("TestUser", "shared", "data") user_shared_dir = os.path.join( - self.configuration.user_home, rel_share_home) + self.configuration.user_home, rel_share_home + ) ensure_dirs_exist(user_shared_dir) share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) @@ -1719,54 +1786,55 @@ def test_refresh_share_creds_no_change_on_unchanged_link(self): chroot=True, access=None, ip_addr=None, - user_dict=None) + user_dict=None, + ) dummy_share.last_update = current_time - self.configuration.daemon_conf['shares'].append(dummy_share) + self.configuration.daemon_conf["shares"].append(dummy_share) - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 1) + self.assertEqual(len(updated_conf["shares"]), 1) def test_refresh_share_creds_detects_missing_link(self): """Test that a missing share link is reported as a change""" # No symlink created - share link is missing - missing_share_id = 'missing123' + missing_share_id = "missing123" - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=missing_share_id, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) # The function should still return an empty list because the link # does not exist; no share is added. self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 0) + self.assertEqual(len(updated_conf["shares"]), 0) def test_refresh_share_creds_ignores_dead_link(self): """Test that a dead share link is ignored""" # Create a symlink that points nowhere - invalid_target = os.path.join(self.configuration.user_home, 'deadbeef') + invalid_target = os.path.join(self.configuration.user_home, "deadbeef") share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) os.symlink(invalid_target, share_link_path) - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) # No share should be added because the link is invalid self.assertEqual(len(changed_shares), 0) - self.assertEqual(len(updated_conf['shares']), 0) + self.assertEqual(len(updated_conf["shares"]), 0) def test_refresh_share_creds_ignores_invalid_link(self): """Test that an invalid (out of bounds) share link is ignored""" @@ -1776,26 +1844,28 @@ def test_refresh_share_creds_ignores_invalid_link(self): share_link_path = os.path.join(self.rw_share_home, TEST_RW_SHARE_ID) os.symlink(invalid_target, share_link_path) - (updated_conf, changed_shares) = refresh_share_creds( + updated_conf, changed_shares = refresh_share_creds( configuration=self.configuration, - protocol='sftp', + protocol="sftp", username=TEST_RW_SHARE_ID, - share_modes=(READ_WRITE_ACCESS, ) + share_modes=(READ_WRITE_ACCESS,), ) # No share should be added because the link is invalid but change # is still reported as modified. self.assertEqual(len(changed_shares), 1) self.assertEqual(changed_shares[0], TEST_RW_SHARE_ID) - self.assertEqual(len(updated_conf['shares']), 0) + self.assertEqual(len(updated_conf["shares"]), 0) -class MigSharedGriddaemonsLogin__refresh_user_creds(MigTestCase, UserAssertMixin): +class MigSharedGriddaemonsLogin__refresh_user_creds( + MigTestCase, UserAssertMixin +): """Unit tests for the griddaemons login refresh_user_creds helper.""" def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test.""" @@ -1812,93 +1882,95 @@ def before_each(self): self.test_user_home = self._provision_test_user(self, TEST_USER_DN) self.test_user_dir = os.path.basename(self.test_user_home) # Make sure alias link is provisioned as well - alias_link_path = os.path.join(self.configuration.user_home, - TEST_USER_EMAIL) + alias_link_path = os.path.join( + self.configuration.user_home, TEST_USER_EMAIL + ) if not os.path.islink(alias_link_path): os.symlink(self.test_user_home, alias_link_path) - ALIAS_FIELD = 'email' + ALIAS_FIELD = "email" self.configuration.user_sftp_alias = ALIAS_FIELD self.configuration.user_ftps_alias = ALIAS_FIELD self.configuration.user_davs_alias = ALIAS_FIELD # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = self.expected_user_db_file - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["users"] = [] + self.configuration.daemon_conf["root_dir"] = ( + self.configuration.user_home + ) + self.configuration.daemon_conf["db_path"] = self.expected_user_db_file + self.configuration.daemon_conf["allow_publickey"] = True + self.configuration.daemon_conf["allow_password"] = True + self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf["user_alias"] = ALIAS_FIELD def test_refresh_user_creds_ssh_protocol(self): """Test refreshing user credentials for SSH protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + _prepare_auth_files(self.test_user_home, ["ssh"]) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ssh", username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] + user_logins = [ + obj + for obj in updated_conf["users"] + if obj.username == TEST_USER_DN or obj.username == username + ] # We expect at least one login (the main username) and possibly aliases self.assertGreaterEqual(len(user_logins), 1) # Check that at least one login has the correct home directory - home_found = any(login.home == self.test_user_dir for login in - user_logins) + home_found = any( + login.home == self.test_user_dir for login in user_logins + ) self.assertTrue(home_found) def test_refresh_user_creds_davs_protocol(self): """Test refreshing user credentials for DAVS protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['davs']) + _prepare_auth_files(self.test_user_home, ["davs"]) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='davs', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="davs", username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] + user_logins = [ + obj for obj in updated_conf["users"] if obj.username == username + ] self.assertEqual(len(user_logins), 1) self.assertEqual(user_logins[0].home, self.test_user_dir) def test_refresh_user_creds_ftps_protocol(self): """Test refreshing user credentials for FTPS protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ftps']) + _prepare_auth_files(self.test_user_home, ["ftps"]) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ftps', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ftps", username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] + user_logins = [ + obj for obj in updated_conf["users"] if obj.username == username + ] self.assertEqual(len(user_logins), 1) self.assertEqual(user_logins[0].home, self.test_user_dir) @@ -1907,31 +1979,34 @@ def test_refresh_user_creds_https_protocol(self): username = TEST_USER_EMAIL # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + updated_conf, changed_users = refresh_user_creds( configuration=self.configuration, - protocol='https', - username=username + protocol="https", + username=username, ) # The user alias should be in the changed list self.assertIn(TEST_USER_EMAIL, changed_users) # Verify that Login objects were added to users for the username and its aliases - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] + user_logins = [ + obj + for obj in updated_conf["users"] + if obj.username == TEST_USER_DN or obj.username == username + ] # We expect at least the main username and possibly aliases self.assertGreaterEqual(len(user_logins), 1) # Check that at least one login has the correct home directory - home_found = any(login.home == self.test_user_dir for login in - user_logins) + home_found = any( + login.home == self.test_user_dir for login in user_logins + ) self.assertTrue(home_found) def test_refresh_user_creds_no_changes(self): """Test that no changes are reported when credentials are unchanged.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + _prepare_auth_files(self.test_user_home, ["ssh"]) # Pre-populate the users list with a Login object that has # last_update set to the current time (simulating no changes) @@ -1946,62 +2021,62 @@ def test_refresh_user_creds_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None) + user_dict=None, + ) dummy_user.last_update = current_time - self.configuration.daemon_conf['users'].append(dummy_user) + self.configuration.daemon_conf["users"].append(dummy_user) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ssh", username=username ) # No changes should be reported self.assertEqual(len(changed_users), 0) # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf['users']), 1) - self.assertEqual(updated_conf['users'][0].username, username) + self.assertEqual(len(updated_conf["users"]), 1) + self.assertEqual(updated_conf["users"][0].username, username) def test_refresh_user_creds_missing_user(self): """Test that a missing user is skipped.""" - username = 'nosuchuser' + username = "nosuchuser" # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ssh", username=username ) # No changes should be reported because the home directory is missing self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) + self.assertEqual(len(updated_conf["users"]), 0) def test_refresh_user_creds_invalid_protocol(self): """Test that an invalid protocol returns early without changes.""" username = TEST_USER_EMAIL # Call the function under test with an invalid protocol - (updated_conf, changed_users) = refresh_user_creds( + updated_conf, changed_users = refresh_user_creds( configuration=self.configuration, - protocol='invalid', - username=username + protocol="invalid", + username=username, ) # No changes should be reported self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) + self.assertEqual(len(updated_conf["users"]), 0) + # TODO: merge X509 and UUID versions? -class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id(MigTestCase, UserAssertMixin): +class MigSharedGriddaemonsLogin__refresh_user_creds_uuid_user_id( + MigTestCase, UserAssertMixin +): """Unit tests for the griddaemons login refresh_user_creds helper with UUID users.""" def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test.""" @@ -2020,86 +2095,92 @@ def before_each(self): overrides = {} overrides["password_hash"] = TEST_USER_PW_HASH - user_dict = _provision_uuid_test_user(self.configuration, TEST_USER_DN, - overrides) - self.test_user_id = user_id = user_dict['unique_id'] + user_dict = _provision_uuid_test_user( + self.configuration, TEST_USER_DN, overrides + ) + self.test_user_id = user_id = user_dict["unique_id"] client_dir = client_id_dir(TEST_USER_DN) - self.test_user_home = os.path.join(self.configuration.user_home, - user_id) + self.test_user_home = os.path.join( + self.configuration.user_home, user_id + ) self.test_user_dir = os.path.basename(self.test_user_home) - self.test_user_home_x509 = os.path.join(self.configuration.user_home, - client_dir) + self.test_user_home_x509 = os.path.join( + self.configuration.user_home, client_dir + ) self.test_user_dir_x509 = os.path.basename(self.test_user_home_x509) # Make sure X509 and alias links are provisioned as well if not os.path.islink(self.test_user_home_x509): os.symlink(self.test_user_home, self.test_user_home_x509) - alias_link_path = os.path.join(self.configuration.user_home, - TEST_USER_EMAIL) + alias_link_path = os.path.join( + self.configuration.user_home, TEST_USER_EMAIL + ) if not os.path.islink(alias_link_path): os.symlink(self.test_user_home_x509, alias_link_path) - ALIAS_FIELD = 'email' + ALIAS_FIELD = "email" self.configuration.user_sftp_alias = ALIAS_FIELD self.configuration.user_ftps_alias = ALIAS_FIELD self.configuration.user_davs_alias = ALIAS_FIELD # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = self.expected_user_db_file - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False - self.configuration.daemon_conf['user_alias'] = ALIAS_FIELD + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["users"] = [] + self.configuration.daemon_conf["root_dir"] = ( + self.configuration.user_home + ) + self.configuration.daemon_conf["db_path"] = self.expected_user_db_file + self.configuration.daemon_conf["allow_publickey"] = True + self.configuration.daemon_conf["allow_password"] = True + self.configuration.daemon_conf["allow_digest"] = False + self.configuration.daemon_conf["user_alias"] = ALIAS_FIELD def test_refresh_user_creds_ssh_protocol(self): """Test refreshing user credentials for SSH protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + _prepare_auth_files(self.test_user_home, ["ssh"]) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ssh", username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] + user_logins = [ + obj + for obj in updated_conf["users"] + if obj.username == TEST_USER_DN or obj.username == username + ] # We expect at least one login (the main username) and possibly aliases self.assertGreaterEqual(len(user_logins), 1) # Check that at least one login has the correct home directory # TODO: is this X509 dir what we want here or the UUID one? - home_found = any(login.home == self.test_user_dir_x509 for login in - user_logins) + home_found = any( + login.home == self.test_user_dir_x509 for login in user_logins + ) self.assertTrue(home_found) def test_refresh_user_creds_davs_protocol(self): """Test refreshing user credentials for DAVS protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['davs']) + _prepare_auth_files(self.test_user_home, ["davs"]) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='davs', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="davs", username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] + user_logins = [ + obj for obj in updated_conf["users"] if obj.username == username + ] self.assertEqual(len(user_logins), 1) # TODO: is this X509 dir what we want here or the UUID one? self.assertEqual(user_logins[0].home, self.test_user_dir_x509) @@ -2107,21 +2188,20 @@ def test_refresh_user_creds_davs_protocol(self): def test_refresh_user_creds_ftps_protocol(self): """Test refreshing user credentials for FTPS protocol.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ftps']) + _prepare_auth_files(self.test_user_home, ["ftps"]) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ftps', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ftps", username=username ) # The user should be in the changed list self.assertIn(username, changed_users) # Verify that Login objects were added to users - user_logins = [obj for obj in updated_conf['users'] - if obj.username == username] + user_logins = [ + obj for obj in updated_conf["users"] if obj.username == username + ] self.assertEqual(len(user_logins), 1) # TODO: is this X509 dir what we want here or the UUID one? self.assertEqual(user_logins[0].home, self.test_user_dir_x509) @@ -2131,32 +2211,35 @@ def test_refresh_user_creds_https_protocol(self): username = TEST_USER_EMAIL # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( + updated_conf, changed_users = refresh_user_creds( configuration=self.configuration, - protocol='https', - username=username + protocol="https", + username=username, ) # The user alias should be in the changed list self.assertIn(TEST_USER_EMAIL, changed_users) # Verify that Login objects were added to users for the username and its aliases - user_logins = [obj for obj in updated_conf['users'] - if obj.username == TEST_USER_DN or - obj.username == username] + user_logins = [ + obj + for obj in updated_conf["users"] + if obj.username == TEST_USER_DN or obj.username == username + ] # We expect at least the main username and possibly aliases self.assertGreaterEqual(len(user_logins), 1) # Check that at least one login has the correct home directory # TODO: is this X509 dir what we want here or the UUID one? - home_found = any(login.home == self.test_user_dir_x509 for login in - user_logins) + home_found = any( + login.home == self.test_user_dir_x509 for login in user_logins + ) self.assertTrue(home_found) def test_refresh_user_creds_no_changes(self): """Test that no changes are reported when credentials are unchanged.""" username = TEST_USER_EMAIL - _prepare_auth_files(self.test_user_home, ['ssh']) + _prepare_auth_files(self.test_user_home, ["ssh"]) # Pre-populate the users list with a Login object that has # last_update set to the current time (simulating no changes) @@ -2171,52 +2254,49 @@ def test_refresh_user_creds_no_changes(self): chroot=True, access=None, ip_addr=None, - user_dict=None) + user_dict=None, + ) dummy_user.last_update = current_time - self.configuration.daemon_conf['users'].append(dummy_user) + self.configuration.daemon_conf["users"].append(dummy_user) # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ssh", username=username ) # No changes should be reported self.assertEqual(len(changed_users), 0) # The user list should still contain only our dummy user - self.assertEqual(len(updated_conf['users']), 1) - self.assertEqual(updated_conf['users'][0].username, username) + self.assertEqual(len(updated_conf["users"]), 1) + self.assertEqual(updated_conf["users"][0].username, username) def test_refresh_user_creds_missing_user(self): """Test that a missing user is skipped.""" - username = 'nosuchuser' + username = "nosuchuser" # Call the function under test - (updated_conf, changed_users) = refresh_user_creds( - configuration=self.configuration, - protocol='ssh', - username=username + updated_conf, changed_users = refresh_user_creds( + configuration=self.configuration, protocol="ssh", username=username ) # No changes should be reported because the home directory is missing self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) + self.assertEqual(len(updated_conf["users"]), 0) def test_refresh_user_creds_invalid_protocol(self): """Test that an invalid protocol returns early without changes.""" username = TEST_USER_EMAIL # Call the function under test with an invalid protocol - (updated_conf, changed_users) = refresh_user_creds( + updated_conf, changed_users = refresh_user_creds( configuration=self.configuration, - protocol='invalid', - username=username + protocol="invalid", + username=username, ) # No changes should be reported self.assertEqual(len(changed_users), 0) - self.assertEqual(len(updated_conf['users']), 0) + self.assertEqual(len(updated_conf["users"]), 0) class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): @@ -2224,62 +2304,62 @@ class MigSharedGriddaemonsLogin__update_login_map(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration with login_map and creds_lock.""" # Common daemon configuration self.configuration.daemon_conf = {} - self.configuration.daemon_conf['time_stamp'] = 0 - self.configuration.daemon_conf['login_map'] = {} - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['jobs'] = [] - self.configuration.daemon_conf['shares'] = [] - self.configuration.daemon_conf['jupyter_mounts'] = [] - self.configuration.daemon_conf['creds_lock'] = threading.Lock() + self.configuration.daemon_conf["time_stamp"] = 0 + self.configuration.daemon_conf["login_map"] = {} + self.configuration.daemon_conf["users"] = [] + self.configuration.daemon_conf["jobs"] = [] + self.configuration.daemon_conf["shares"] = [] + self.configuration.daemon_conf["jupyter_mounts"] = [] + self.configuration.daemon_conf["creds_lock"] = threading.Lock() def test_update_login_map_users(self): """Verify login_map is updated correctly for changed users.""" # Create Login objects user1 = Login( configuration=self.configuration, - username='user1', - home='home1', + username="user1", + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) user2 = Login( configuration=self.configuration, - username='user2', - home='home2', + username="user2", + home="home2", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) # Populate users list - self.configuration.daemon_conf['users'] = [user1, user2] + self.configuration.daemon_conf["users"] = [user1, user2] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, - changed_users=['user1', 'user2'] + changed_users=["user1", "user2"], ) # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('user1', login_map) - self.assertIn('user2', login_map) - self.assertEqual(login_map['user1'], [user1]) - self.assertEqual(login_map['user2'], [user2]) + login_map = self.configuration.daemon_conf["login_map"] + self.assertIn("user1", login_map) + self.assertIn("user2", login_map) + self.assertEqual(login_map["user1"], [user1]) + self.assertEqual(login_map["user2"], [user2]) def test_update_login_map_jobs(self): """Verify login_map is updated correctly for changed jobs.""" @@ -2287,39 +2367,39 @@ def test_update_login_map_jobs(self): test_job = Login( configuration=self.configuration, username=TEST_JOB_ID, - home='home1', + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, - ip_addr='1.2.3.4', - user_dict=None + ip_addr="1.2.3.4", + user_dict=None, ) other_job = Login( configuration=self.configuration, username=OTHER_JOB_ID, - home='home2', + home="home2", password=None, digest=None, public_key=None, chroot=True, access=None, - ip_addr='5.6.7.8', - user_dict=None + ip_addr="5.6.7.8", + user_dict=None, ) # Populate jobs list - self.configuration.daemon_conf['jobs'] = [test_job, other_job] + self.configuration.daemon_conf["jobs"] = [test_job, other_job] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_jobs=[TEST_JOB_ID, OTHER_JOB_ID] + changed_jobs=[TEST_JOB_ID, OTHER_JOB_ID], ) # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] + login_map = self.configuration.daemon_conf["login_map"] self.assertIn(TEST_JOB_ID, login_map) self.assertIn(OTHER_JOB_ID, login_map) self.assertEqual(login_map[TEST_JOB_ID], [test_job]) @@ -2330,44 +2410,44 @@ def test_update_login_map_shares(self): # Create Login objects share1 = Login( configuration=self.configuration, - username='share1', - home='home1', + username="share1", + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) share2 = Login( configuration=self.configuration, - username='share2', - home='home2', + username="share2", + home="home2", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) # Populate shares list - self.configuration.daemon_conf['shares'] = [share1, share2] + self.configuration.daemon_conf["shares"] = [share1, share2] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_shares=['share1', 'share2'] + changed_shares=["share1", "share2"], ) # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('share1', login_map) - self.assertIn('share2', login_map) - self.assertEqual(login_map['share1'], [share1]) - self.assertEqual(login_map['share2'], [share2]) + login_map = self.configuration.daemon_conf["login_map"] + self.assertIn("share1", login_map) + self.assertIn("share2", login_map) + self.assertEqual(login_map["share1"], [share1]) + self.assertEqual(login_map["share2"], [share2]) def test_update_login_map_jupyter(self): """Verify login_map is updated correctly for changed jupyter mounts.""" @@ -2375,26 +2455,26 @@ def test_update_login_map_jupyter(self): test_session = Login( configuration=self.configuration, username=TEST_JUPYTER_SESSION_ID, - home='home1', + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) other_session = Login( configuration=self.configuration, username=OTHER_JUPYTER_SESSION_ID, - home='home2', + home="home2", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) # Populate jupyter_mounts list self.configuration.daemon_conf['jupyter_mounts'] = [ @@ -2403,11 +2483,11 @@ def test_update_login_map_jupyter(self): update_login_map( daemon_conf=self.configuration.daemon_conf, changed_users=[], - changed_jupyter=[TEST_JUPYTER_SESSION_ID, OTHER_JUPYTER_SESSION_ID] + changed_jupyter=[TEST_JUPYTER_SESSION_ID, OTHER_JUPYTER_SESSION_ID], ) # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] + login_map = self.configuration.daemon_conf["login_map"] self.assertIn(TEST_JUPYTER_SESSION_ID, login_map) self.assertIn(OTHER_JUPYTER_SESSION_ID, login_map) self.assertEqual(login_map[TEST_JUPYTER_SESSION_ID], [test_session]) @@ -2418,45 +2498,45 @@ def test_update_login_map_nonexistent(self): # Populate users list with one user user1 = Login( configuration=self.configuration, - username='user1', - home='home1', + username="user1", + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) - self.configuration.daemon_conf['users'] = [user1] + self.configuration.daemon_conf["users"] = [user1] # Call the function under test for a non-existent user update_login_map( daemon_conf=self.configuration.daemon_conf, - changed_users=['nonexistent'] + changed_users=["nonexistent"], ) # Verify login_map - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('nonexistent', login_map) - self.assertEqual(login_map['nonexistent'], []) + login_map = self.configuration.daemon_conf["login_map"] + self.assertIn("nonexistent", login_map) + self.assertEqual(login_map["nonexistent"], []) def test_update_login_map_empty_lists(self): """Verify login_map is not changed when changed lists are empty.""" # Populate users list user1 = Login( configuration=self.configuration, - username='user1', - home='home1', + username="user1", + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) - self.configuration.daemon_conf['users'] = [user1] + self.configuration.daemon_conf["users"] = [user1] # Call the function under test with empty lists update_login_map( @@ -2464,11 +2544,11 @@ def test_update_login_map_empty_lists(self): changed_users=[], changed_jobs=[], changed_shares=[], - changed_jupyter=[] + changed_jupyter=[], ) # Verify login_map is unchanged (should be empty because we didn't update for 'user1') - login_map = self.configuration.daemon_conf['login_map'] + login_map = self.configuration.daemon_conf["login_map"] self.assertEqual(len(login_map), 0) def test_update_login_map_lock_handling(self): @@ -2476,28 +2556,27 @@ def test_update_login_map_lock_handling(self): # Create a Login object user1 = Login( configuration=self.configuration, - username='user1', - home='home1', + username="user1", + home="home1", password=None, digest=None, public_key=None, chroot=True, access=None, ip_addr=None, - user_dict=None + user_dict=None, ) - self.configuration.daemon_conf['users'] = [user1] + self.configuration.daemon_conf["users"] = [user1] # Call the function under test update_login_map( - daemon_conf=self.configuration.daemon_conf, - changed_users=['user1'] + daemon_conf=self.configuration.daemon_conf, changed_users=["user1"] ) # Verify login_map was updated (lock must have been acquired/released) - login_map = self.configuration.daemon_conf['login_map'] - self.assertIn('user1', login_map) - self.assertEqual(login_map['user1'], [user1]) + login_map = self.configuration.daemon_conf["login_map"] + self.assertIn("user1", login_map) + self.assertEqual(login_map["user1"], [user1]) class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): @@ -2505,40 +2584,48 @@ class MigSharedGriddaemonsLogin__update_user_objects(MigTestCase): def _provide_configuration(self): """Return a test configuration instance.""" - return 'testconfig' + return "testconfig" def before_each(self): """Set up test configuration and reset state before each test.""" # Ensure required directories exist _ensure_dirs_needed_for_userdb(self.configuration) self.configuration.daemon_conf = {} - self.configuration.daemon_conf['users'] = [] - self.configuration.daemon_conf['root_dir'] = self.configuration.user_home - self.configuration.daemon_conf['db_path'] = os.path.join( - self.configuration.user_db_home, "MiG-users.db") - self.configuration.daemon_conf['allow_publickey'] = True - self.configuration.daemon_conf['allow_password'] = True - self.configuration.daemon_conf['allow_digest'] = False + self.configuration.daemon_conf["users"] = [] + self.configuration.daemon_conf["root_dir"] = ( + self.configuration.user_home + ) + self.configuration.daemon_conf["db_path"] = os.path.join( + self.configuration.user_db_home, "MiG-users.db" + ) + self.configuration.daemon_conf["allow_publickey"] = True + self.configuration.daemon_conf["allow_password"] = True + self.configuration.daemon_conf["allow_digest"] = False # Create a test user home self.test_user_home = self._provision_test_user(self, TEST_USER_DN) self.test_user_dir = os.path.basename(self.test_user_home) # Create auth files for the user - self.ssh_auth_paths = _prepare_auth_files(self.test_user_home, ['ssh']) + self.ssh_auth_paths = _prepare_auth_files(self.test_user_home, ["ssh"]) self.ssh_auth_keys_path, self.ssh_auth_pw_path = self.ssh_auth_paths def test_update_user_objects_adds_passwords(self): """Verify that update_user_objects adds Login objects for passwords.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_keys part from auth_keys_path - authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') + authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, "") authkeys = authkeys.lstrip(os.sep) - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") authpw = authpw.lstrip(os.sep) authprotos = (authkeys, authpw, None) - user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, - TEST_USER_SHORT_ID, TEST_USER_EMAIL) + user_tuple = ( + TEST_USER_DN, + TEST_USER_EMAIL, + self.test_user_dir, + TEST_USER_SHORT_ID, + TEST_USER_EMAIL, + ) # Call the helper update_user_objects( configuration=self.configuration, @@ -2546,39 +2633,48 @@ def test_update_user_objects_adds_passwords(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True + private_auth_file=True, ) # Verify that three aliased pw logins were added - pw_logins = [u for u in daemon_conf['users'] if u.password is not None] + pw_logins = [u for u in daemon_conf["users"] if u.password is not None] self.assertEqual(len(pw_logins), 3) for entry in pw_logins: - self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, - TEST_USER_SHORT_ID)) + self.assertIn( + entry.username, + (TEST_USER_DN, TEST_USER_EMAIL, TEST_USER_SHORT_ID), + ) self.assertEqual(entry.home, self.test_user_dir) self.assertEqual(entry.access, READ_WRITE_ACCESS) self.assertEqual(entry.password, TEST_USER_PW_HASH) # Verify that no key logins were added - key_logins = [u for u in daemon_conf['users'] - if u.public_key is not None] + key_logins = [ + u for u in daemon_conf["users"] if u.public_key is not None + ] self.assertEqual(len(key_logins), 0) # Verify that no digest logins were added - digest_logins = [u for u in daemon_conf['users'] - if u.digest is not None] + digest_logins = [ + u for u in daemon_conf["users"] if u.digest is not None + ] self.assertEqual(len(digest_logins), 0) def test_update_user_objects_adds_keys(self): """Verify that update_user_objects adds Login objects for keys.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_keys part from auth_keys_path - authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, '') + authkeys = self.ssh_auth_keys_path.replace(self.test_user_home, "") authkeys = authkeys.lstrip(os.sep) - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") authpw = authpw.lstrip(os.sep) authprotos = (authkeys, authpw, None) - user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, - TEST_USER_SHORT_ID, TEST_USER_EMAIL) + user_tuple = ( + TEST_USER_DN, + TEST_USER_EMAIL, + self.test_user_dir, + TEST_USER_SHORT_ID, + TEST_USER_EMAIL, + ) # Call the helper update_user_objects( configuration=self.configuration, @@ -2586,17 +2682,20 @@ def test_update_user_objects_adds_keys(self): path=self.ssh_auth_keys_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True + private_auth_file=True, ) # Verify that three aliased key logins were added (dupe keys ignored) - key_logins = [u for u in daemon_conf['users'] if u.public_key is not - None] + key_logins = [ + u for u in daemon_conf["users"] if u.public_key is not None + ] self.assertEqual(len(key_logins), 3) for entry in key_logins: - self.assertIn(entry.username, (TEST_USER_DN, TEST_USER_EMAIL, - TEST_USER_SHORT_ID)) + self.assertIn( + entry.username, + (TEST_USER_DN, TEST_USER_EMAIL, TEST_USER_SHORT_ID), + ) self.assertEqual(entry.home, self.test_user_dir) self.assertEqual(entry.access, READ_WRITE_ACCESS) login_key = entry.public_key @@ -2604,22 +2703,28 @@ def test_update_user_objects_adds_keys(self): self.assertEqual(result, TEST_USER_PUB_KEY) # Verify that no password logins were added - pw_logins = [u for u in daemon_conf['users'] if u.password is not None] + pw_logins = [u for u in daemon_conf["users"] if u.password is not None] self.assertEqual(len(pw_logins), 0) # Verify that no digest logins were added - digest_logins = [u for u in daemon_conf['users'] - if u.digest is not None] + digest_logins = [ + u for u in daemon_conf["users"] if u.digest is not None + ] self.assertEqual(len(digest_logins), 0) def test_update_user_objects_removes_old_entries(self): """Verify that update_user_objects cleans old entries for the same user.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_passwords part from auth_pw_path - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") authpw = authpw.lstrip(os.sep) authprotos = (None, authpw, None) - user_tuple = (TEST_USER_DN, TEST_USER_EMAIL, self.test_user_dir, - None, None) + user_tuple = ( + TEST_USER_DN, + TEST_USER_EMAIL, + self.test_user_dir, + None, + None, + ) # Create a dummy user with a last_update in the past to remove in test past_timestamp = time.time() - 3600 dummy_user = Login( @@ -2627,10 +2732,10 @@ def test_update_user_objects_removes_old_entries(self): username=TEST_USER_EMAIL, home=self.test_user_dir, password=TEST_USER_PW_HASH, - access=READ_WRITE_ACCESS + access=READ_WRITE_ACCESS, ) dummy_user.last_update = past_timestamp - daemon_conf['users'].append(dummy_user) + daemon_conf["users"].append(dummy_user) # Remove saved password os.remove(self.ssh_auth_pw_path) @@ -2641,22 +2746,21 @@ def test_update_user_objects_removes_old_entries(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True + private_auth_file=True, ) # Old entry should be removed - usernames = [u.username for u in daemon_conf['users']] + usernames = [u.username for u in daemon_conf["users"]] self.assertNotIn(TEST_USER_DN, usernames, "Old entry was not removed") def test_update_user_objects_no_changes(self): """Verify that calling update_user_objects with unchanged files does not duplicate entries.""" daemon_conf = self.configuration.daemon_conf # Extract the .PROTO/authorized_passwords part from auth_pw_path - authpw = self.ssh_auth_pw_path.replace(self.test_user_home, '') + authpw = self.ssh_auth_pw_path.replace(self.test_user_home, "") authpw = authpw.lstrip(os.sep) authprotos = (None, authpw, None) - user_tuple = (TEST_USER_DN, None, self.test_user_dir, - None, None) + user_tuple = (TEST_USER_DN, None, self.test_user_dir, None, None) # First call to populate update_user_objects( configuration=self.configuration, @@ -2664,9 +2768,9 @@ def test_update_user_objects_no_changes(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True + private_auth_file=True, ) - initial_count = len(daemon_conf['users']) + initial_count = len(daemon_conf["users"]) # Second call with same files update_user_objects( @@ -2675,11 +2779,14 @@ def test_update_user_objects_no_changes(self): path=self.ssh_auth_pw_path, user_vars=user_tuple, auth_protos=authprotos, - private_auth_file=True + private_auth_file=True, + ) + self.assertEqual( + len(daemon_conf["users"]), + initial_count, + "Duplicate entries were added on second call", ) - self.assertEqual(len(daemon_conf['users']), initial_count, - "Duplicate entries were added on second call") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 3053cf1fc42d9547d2333f9afc8a315d126b4f90 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 16:38:54 +0200 Subject: [PATCH 39/61] Pull in some minor adjustments to useramd test suite from #576. --- tests/test_mig_shared_useradm.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index a48d0e976..3e634913b 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -38,7 +38,8 @@ # Imports required for the unit test wrapping from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( - DEFAULT_USER_ID_FORMAT, UUID_USER_ID_FORMAT, + DEFAULT_USER_ID_FORMAT, + UUID_USER_ID_FORMAT, htaccess_filename, keyword_auto, ssh_conf_dir, @@ -660,7 +661,7 @@ def test_user_creation_records_a_user_with_gdp(self): create_user( user_dict, self.configuration, keyword_auto, default_renew=True ) - except: + except Exception: self.assertFalse(True, "should not be reached") def test_user_creation_and_renew_records_a_user(self): @@ -685,7 +686,7 @@ def test_user_creation_and_renew_records_a_user(self): default_renew=True, ask_renew=False, ) - except: + except Exception: self.assertFalse(True, "should not be reached") try: @@ -696,7 +697,7 @@ def test_user_creation_and_renew_records_a_user(self): default_renew=True, ask_renew=False, ) - except: + except Exception: self.assertFalse(True, "should not be reached") def test_user_creation_fails_in_renew_when_locked(self): @@ -724,7 +725,7 @@ def test_user_creation_fails_in_renew_when_locked(self): default_renew=True, ask_renew=False, ) - except: + except Exception: self.assertFalse(True, "should not be reached") def test_user_creation_with_id_collission_fails(self): @@ -743,7 +744,7 @@ def test_user_creation_with_id_collission_fails(self): create_user( user_dict, self.configuration, keyword_auto, default_renew=True ) - except: + except Exception: self.assertFalse(True, "should not be reached") # NOTE: reset distinguished_name and introduce an ID conflict to test @@ -782,7 +783,7 @@ def assertHtaccessRequireUserClause(self, generated, expected): generated = htaccess_file.read() generated_lines = generated.split("\n") - if not expected in generated_lines: + if expected not in generated_lines: raise AssertionError("no such require user line: %s" % expected) def test_skips_accounts_without_short_id(self): @@ -796,7 +797,7 @@ def test_skips_accounts_without_short_id(self): path_kind = self.assertPathExists(DUMMY_REL_HTACCESS_PATH) # File should not exist here at all self.assertNotEqual(path_kind, "file") - except OSError as ignore_oserr: + except OSError: pass def test_creates_missing_htaccess_file(self): @@ -989,14 +990,10 @@ def test_user_deletion_removes_fs_entries(self): self.assertFalse(os.path.exists(short_link)) settings_dir = os.path.join(self.configuration.user_settings, - TEST_USER_UUID) + TEST_USER_DIR) self.assertFalse(os.path.isdir(settings_dir)) self.assertFalse(os.path.exists(settings_dir)) - settings_link = os.path.join(self.configuration.user_settings, - TEST_USER_DIR) - self.assertFalse(os.path.islink(settings_link)) - self.assertFalse(os.path.exists(settings_link)) - + ssh_dir = os.path.join(home_dir, ssh_conf_dir) self.assertFalse(os.path.isdir(ssh_dir)) self.assertFalse(os.path.exists(ssh_dir)) From 8009fe13e5258838bda041a545f4ca8c0bff0f04 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 19:44:45 +0200 Subject: [PATCH 40/61] Initial unit tests for accountstate. --- tests/test_mig_shared_accountstate.py | 190 ++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 tests/test_mig_shared_accountstate.py diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py new file mode 100644 index 000000000..49aec7782 --- /dev/null +++ b/tests/test_mig_shared_accountstate.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +# +# --- BEGIN_HEADER --- +# +# test_mig_shared_accountstate - unit test of the corresponding mig shared module +# Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH +# +# This file is part of MiG. +# +# MiG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# MiG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. +# +# --- END_HEADER --- +# + +"""Unit tests for the mig shared accountstate module""" + +import time + +from mig.shared.accountstate import ( + default_account_valid_days, + default_account_expire, +) +from mig.shared.defaults import ( + AUTH_CERTIFICATE, + AUTH_OPENID_V2, + AUTH_OPENID_CONNECT, + AUTH_GENERIC, +) + +from tests.support import MigTestCase + + +class TestMigSharedAccountstate__default_account_valid_days(MigTestCase): + """Coverage of accountstate default_account_valid_days function.""" + + def _provide_configuration(self): + return "testconfig" + + def test_returns_cert_valid_days_for_certificate_auth(self): + """Test that cert_valid_days is returned for AUTH_CERTIFICATE.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + result = default_account_valid_days(configuration, AUTH_CERTIFICATE) + self.assertEqual(result, 365) + + def test_returns_oid_valid_days_for_openid_v2_auth(self): + """Test that oid_valid_days is returned for AUTH_OPENID_V2.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + result = default_account_valid_days(configuration, AUTH_OPENID_V2) + self.assertEqual(result, 30) + + def test_returns_oidc_valid_days_for_openid_connect_auth(self): + """Test that oidc_valid_days is returned for AUTH_OPENID_CONNECT.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + result = default_account_valid_days(configuration, AUTH_OPENID_CONNECT) + self.assertEqual(result, 30) + + def test_returns_generic_valid_days_for_generic_auth(self): + """Test that generic_valid_days is returned for AUTH_GENERIC.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + result = default_account_valid_days(configuration, AUTH_GENERIC) + self.assertEqual(result, 14) + + def test_returns_generic_valid_days_for_unknown_auth_type(self): + """Test that generic_valid_days is fallback for unknown auth type.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + result = default_account_valid_days(configuration, "UNKNOWN_AUTH_TYPE") + self.assertEqual(result, 14) + + +class TestMigSharedAccountstate__default_account_expire(MigTestCase): + """Coverage of accountstate default_account_expire function.""" + + def _provide_configuration(self): + return "testconfig" + + def test_calculates_expire_from_valid_days(self): + """Test that expire is calculated correctly from valid days.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + start_time = time.time() + result = default_account_expire(configuration, AUTH_CERTIFICATE, + start_time=start_time) + expected = int(start_time + 365 * 24 * 60 * 60) + self.assertEqual(result, expected) + + def test_calculates_expire_for_openid_v2(self): + """Test expire calculation for AUTH_OPENID_V2.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + start_time = time.time() + result = default_account_expire(configuration, AUTH_OPENID_V2, + start_time=start_time) + expected = int(start_time + 30 * 24 * 60 * 60) + self.assertEqual(result, expected) + + def test_calculates_expire_for_openid_connect(self): + """Test expire calculation for AUTH_OPENID_CONNECT.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + start_time = time.time() + result = default_account_expire(configuration, AUTH_OPENID_CONNECT, + start_time=start_time) + expected = int(start_time + 30 * 24 * 60 * 60) + self.assertEqual(result, expected) + + def test_calculates_expire_for_generic_auth(self): + """Test expire calculation for AUTH_GENERIC.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + start_time = time.time() + result = default_account_expire(configuration, AUTH_GENERIC, + start_time=start_time) + expected = int(start_time + 14 * 24 * 60 * 60) + self.assertEqual(result, expected) + + def test_uses_current_time_if_start_time_not_provided(self): + """Test that current time is used when start_time is not provided.""" + configuration = self.configuration + configuration.cert_valid_days = 365 + configuration.oid_valid_days = 30 + configuration.oidc_valid_days = 30 + configuration.generic_valid_days = 14 + + before = int(time.time()) + result = default_account_expire(configuration, AUTH_CERTIFICATE) + after = int(time.time()) + + # Result should be between before + 365 days and after + 365 days + min_expected = before + 365 * 24 * 60 * 60 + max_expected = after + 365 * 24 * 60 * 60 + self.assertTrue(min_expected <= result <= max_expected) + + +if __name__ == "__main__": + from tests.support import testmain + testmain() From a99675d3319c022b43f2b6df9629095fb34c6086 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 20:35:48 +0200 Subject: [PATCH 41/61] Extend accountstate unit tests to cover get_account_expire_cache, update_account_expire_cache and reset_account_expire_cache. Simiplifi existing tests to rely more on before_each helper and constants. Fix a bug in reset_account_expire_cache for specific client_id which wasn't correctly translated from client_id to a client_dir style filemark. --- mig/shared/accountstate.py | 6 +- tests/test_mig_shared_accountstate.py | 287 ++++++++++++++++++++++---- 2 files changed, 254 insertions(+), 39 deletions(-) diff --git a/mig/shared/accountstate.py b/mig/shared/accountstate.py index c7198925e..8cf30ff51 100644 --- a/mig/shared/accountstate.py +++ b/mig/shared/accountstate.py @@ -118,7 +118,11 @@ def reset_account_expire_cache(configuration, client_id=None): _logger = configuration.logger res = True base_dir = os.path.join(configuration.mig_system_run, expire_marks_dir) - reset_filemark(configuration, base_dir, client_id) + if client_id is None: + target = client_id + else: + target = client_id_dir(client_id) + reset_filemark(configuration, base_dir, target) return res diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 49aec7782..1e7864ac5 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -28,19 +28,40 @@ """Unit tests for the mig shared accountstate module""" import time +import os +# Imports of the code under test from mig.shared.accountstate import ( default_account_valid_days, default_account_expire, + update_account_expire_cache, + get_account_expire_cache, + reset_account_expire_cache, ) + +# Imports required for the unit test wrapping +from mig.shared.base import client_id_dir from mig.shared.defaults import ( AUTH_CERTIFICATE, AUTH_OPENID_V2, AUTH_OPENID_CONNECT, AUTH_GENERIC, + expire_marks_dir, +) + +# Imports required for the unit tests themselves +from tests.support import ( + MigTestCase, + testmain, + ensure_dirs_exist, ) +from tests.support.usersupp import TEST_USER_DN, OTHER_USER_DN, UserAssertMixin -from tests.support import MigTestCase +# Test constants +TEST_EXPIRE_TIMESTAMP = 1776031200 + +TEST_USER_DIR = client_id_dir(TEST_USER_DN) +OTHER_USER_DIR = client_id_dir(OTHER_USER_DN) class TestMigSharedAccountstate__default_account_valid_days(MigTestCase): @@ -49,24 +70,23 @@ class TestMigSharedAccountstate__default_account_valid_days(MigTestCase): def _provide_configuration(self): return "testconfig" - def test_returns_cert_valid_days_for_certificate_auth(self): - """Test that cert_valid_days is returned for AUTH_CERTIFICATE.""" + def before_each(self): configuration = self.configuration configuration.cert_valid_days = 365 configuration.oid_valid_days = 30 configuration.oidc_valid_days = 30 configuration.generic_valid_days = 14 + def test_returns_cert_valid_days_for_certificate_auth(self): + """Test that cert_valid_days is returned for AUTH_CERTIFICATE.""" + configuration = self.configuration + result = default_account_valid_days(configuration, AUTH_CERTIFICATE) self.assertEqual(result, 365) def test_returns_oid_valid_days_for_openid_v2_auth(self): """Test that oid_valid_days is returned for AUTH_OPENID_V2.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 result = default_account_valid_days(configuration, AUTH_OPENID_V2) self.assertEqual(result, 30) @@ -74,10 +94,6 @@ def test_returns_oid_valid_days_for_openid_v2_auth(self): def test_returns_oidc_valid_days_for_openid_connect_auth(self): """Test that oidc_valid_days is returned for AUTH_OPENID_CONNECT.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 result = default_account_valid_days(configuration, AUTH_OPENID_CONNECT) self.assertEqual(result, 30) @@ -85,10 +101,6 @@ def test_returns_oidc_valid_days_for_openid_connect_auth(self): def test_returns_generic_valid_days_for_generic_auth(self): """Test that generic_valid_days is returned for AUTH_GENERIC.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 result = default_account_valid_days(configuration, AUTH_GENERIC) self.assertEqual(result, 14) @@ -96,10 +108,6 @@ def test_returns_generic_valid_days_for_generic_auth(self): def test_returns_generic_valid_days_for_unknown_auth_type(self): """Test that generic_valid_days is fallback for unknown auth type.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 result = default_account_valid_days(configuration, "UNKNOWN_AUTH_TYPE") self.assertEqual(result, 14) @@ -111,14 +119,17 @@ class TestMigSharedAccountstate__default_account_expire(MigTestCase): def _provide_configuration(self): return "testconfig" - def test_calculates_expire_from_valid_days(self): - """Test that expire is calculated correctly from valid days.""" + def before_each(self): configuration = self.configuration configuration.cert_valid_days = 365 configuration.oid_valid_days = 30 configuration.oidc_valid_days = 30 configuration.generic_valid_days = 14 + def test_calculates_expire_from_valid_days(self): + """Test that expire is calculated correctly from valid days.""" + configuration = self.configuration + start_time = time.time() result = default_account_expire(configuration, AUTH_CERTIFICATE, start_time=start_time) @@ -128,10 +139,6 @@ def test_calculates_expire_from_valid_days(self): def test_calculates_expire_for_openid_v2(self): """Test expire calculation for AUTH_OPENID_V2.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 start_time = time.time() result = default_account_expire(configuration, AUTH_OPENID_V2, @@ -142,10 +149,6 @@ def test_calculates_expire_for_openid_v2(self): def test_calculates_expire_for_openid_connect(self): """Test expire calculation for AUTH_OPENID_CONNECT.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 start_time = time.time() result = default_account_expire(configuration, AUTH_OPENID_CONNECT, @@ -156,10 +159,6 @@ def test_calculates_expire_for_openid_connect(self): def test_calculates_expire_for_generic_auth(self): """Test expire calculation for AUTH_GENERIC.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 start_time = time.time() result = default_account_expire(configuration, AUTH_GENERIC, @@ -170,10 +169,6 @@ def test_calculates_expire_for_generic_auth(self): def test_uses_current_time_if_start_time_not_provided(self): """Test that current time is used when start_time is not provided.""" configuration = self.configuration - configuration.cert_valid_days = 365 - configuration.oid_valid_days = 30 - configuration.oidc_valid_days = 30 - configuration.generic_valid_days = 14 before = int(time.time()) result = default_account_expire(configuration, AUTH_CERTIFICATE) @@ -185,6 +180,222 @@ def test_uses_current_time_if_start_time_not_provided(self): self.assertTrue(min_expected <= result <= max_expected) +class TestMigSharedAccountstate__update_account_expire_cache(MigTestCase): + """Coverage of accountstate update_account_expire_cache function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for expire cache tests.""" + configuration = self.configuration + # Ensure the mig_system_run sub directory exists for expire marks + marks_path = os.path.join(configuration.mig_system_run, + expire_marks_dir) + ensure_dirs_exist(marks_path) + + def test_update_account_expire_cache_creates_mark(self): + """Test that update_account_expire_cache creates an expire mark.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": TEST_EXPIRE_TIMESTAMP, + } + + result = update_account_expire_cache(configuration, user_dict) + self.assertTrue(result) + + # Verify the mark was created + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, TEST_EXPIRE_TIMESTAMP) + + def test_update_account_expire_cache_with_missing_client_id(self): + """Test that update fails gracefully when client_id is missing.""" + configuration = self.configuration + user_dict = { + "expire": TEST_EXPIRE_TIMESTAMP, + } + + with self.assertLogs(level='ERROR') as log_capture: + result = update_account_expire_cache(configuration, user_dict) + self.assertFalse(result) + self.assertTrue(any('no client ID' in msg + for msg in log_capture.output)) + + def test_update_account_expire_cache_with_missing_expire(self): + """Test that update returns True when expire is missing.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + } + + with self.assertLogs(level='INFO') as log_capture: + result = update_account_expire_cache(configuration, user_dict) + self.assertTrue(result) + self.assertTrue(any('no expire set' in msg + for msg in log_capture.output)) + + def test_update_account_expire_cache_with_string_expire(self): + """Test that update fails when expire is a string.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": "not_a_number", + } + + with self.assertLogs(level='WARNING') as log_capture: + result = update_account_expire_cache(configuration, user_dict) + self.assertFalse(result) + self.assertTrue(any('string expire value' in msg + for msg in log_capture.output)) + + def test_update_account_expire_cache_with_delete(self): + """Test that delete=True removes the expire mark.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": TEST_EXPIRE_TIMESTAMP, + } + + # First create the mark + update_account_expire_cache(configuration, user_dict) + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, TEST_EXPIRE_TIMESTAMP) + + # Then delete it + result = update_account_expire_cache( + configuration, user_dict, delete=True) + self.assertTrue(result) + + # Verify the mark was removed + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertIsNone(cached_expire) + + def test_update_account_expire_cache_with_invalid_user_dict(self): + """Test that update fails when user_dict is not a dictionary.""" + configuration = self.configuration + + with self.assertLogs(level='ERROR') as log_capture: + result = update_account_expire_cache(configuration, "not_a_dict") + self.assertFalse(result) + self.assertTrue(any('invalid user_dict' in msg + for msg in log_capture.output)) + + +class TestMigSharedAccountstate__get_account_expire_cache(MigTestCase): + """Coverage of accountstate get_account_expire_cache function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for expire cache tests.""" + configuration = self.configuration + # Ensure the mig_system_run sub directory exists for expire marks + marks_path = os.path.join(configuration.mig_system_run, + expire_marks_dir) + ensure_dirs_exist(marks_path) + + def test_get_account_expire_cache_returns_cached_value(self): + """Test that get_account_expire_cache returns cached expire value.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": TEST_EXPIRE_TIMESTAMP, + } + + # Create the mark first + update_account_expire_cache(configuration, user_dict) + + # Then retrieve it + result = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(result, TEST_EXPIRE_TIMESTAMP) + + def test_get_account_expire_cache_with_missing_client_id(self): + """Test that get fails gracefully when client_id is missing.""" + configuration = self.configuration + + with self.assertLogs(level='ERROR') as log_capture: + result = get_account_expire_cache(configuration, "") + self.assertFalse(result) + self.assertTrue(any('invalid client ID' in msg + for msg in log_capture.output)) + + +class TestMigSharedAccountstate__reset_account_expire_cache(MigTestCase): + """Coverage of accountstate reset_account_expire_cache function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for expire cache tests.""" + configuration = self.configuration + # Ensure the mig_system_run sub directory exists for expire marks + marks_path = os.path.join(configuration.mig_system_run, + expire_marks_dir) + ensure_dirs_exist(marks_path) + + self.expire_base = os.path.join(configuration.mig_system_run, + expire_marks_dir) + + def test_reset_account_expire_cache_resets_all_marks(self): + """Test that reset_account_expire_cache resets all expire marks.""" + configuration = self.configuration + # Create a couple of marks first + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": TEST_EXPIRE_TIMESTAMP, + } + update_account_expire_cache(configuration, user_dict) + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, TEST_EXPIRE_TIMESTAMP) + user_dict = { + "distinguished_name": OTHER_USER_DN, + "expire": TEST_EXPIRE_TIMESTAMP + 42, + } + update_account_expire_cache(configuration, user_dict) + cached_expire = get_account_expire_cache(configuration, OTHER_USER_DN) + self.assertEqual(cached_expire, TEST_EXPIRE_TIMESTAMP + 42) + + # Then reset all + result = reset_account_expire_cache(configuration) + self.assertTrue(result) + + # Verify all marks were reset but not removed + marks_path = os.path.join(self.expire_base, TEST_USER_DIR) + self.assertTrue(os.path.exists(marks_path)) + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, 0.0) + marks_path = os.path.join(self.expire_base, OTHER_USER_DIR) + self.assertTrue(os.path.exists(marks_path)) + cached_expire = get_account_expire_cache(configuration, OTHER_USER_DN) + self.assertEqual(cached_expire, 0.0) + + def test_reset_account_expire_cache_resets_specific_mark(self): + """Test that reset_account_expire_cache resets given expire mark.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": TEST_EXPIRE_TIMESTAMP, + } + + marks_path = os.path.join(self.expire_base, TEST_USER_DIR) + # Create the mark first + update_account_expire_cache(configuration, user_dict) + self.assertTrue(os.path.exists(marks_path)) + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, TEST_EXPIRE_TIMESTAMP) + + # Then reset it + result = reset_account_expire_cache(configuration, TEST_USER_DN) + self.assertTrue(result) + + # Verify the mark was reset but not removed + self.assertTrue(os.path.exists(marks_path)) + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, 0.0) + + if __name__ == "__main__": - from tests.support import testmain testmain() From 622fb5d879ba3d5f3b1310c5a6261f3c6d6b0c04 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 20:46:06 +0200 Subject: [PATCH 42/61] Extend with unit tests for get_account_status_cache and update_account_status_cache . --- tests/test_mig_shared_accountstate.py | 171 +++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 1 deletion(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 1e7864ac5..17c651181 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -37,6 +37,8 @@ update_account_expire_cache, get_account_expire_cache, reset_account_expire_cache, + get_account_status_cache, + update_account_status_cache ) # Imports required for the unit test wrapping @@ -47,6 +49,7 @@ AUTH_OPENID_CONNECT, AUTH_GENERIC, expire_marks_dir, + status_marks_dir ) # Imports required for the unit tests themselves @@ -55,10 +58,12 @@ testmain, ensure_dirs_exist, ) -from tests.support.usersupp import TEST_USER_DN, OTHER_USER_DN, UserAssertMixin +from tests.support.usersupp import TEST_USER_DN, OTHER_USER_DN # Test constants TEST_EXPIRE_TIMESTAMP = 1776031200 +TEST_STATUS_ACTIVE = 'active' +TEST_STATUS_LOCKED = 'locked' TEST_USER_DIR = client_id_dir(TEST_USER_DN) OTHER_USER_DIR = client_id_dir(OTHER_USER_DN) @@ -397,5 +402,169 @@ def test_reset_account_expire_cache_resets_specific_mark(self): self.assertEqual(cached_expire, 0.0) +class TestMigSharedAccountstate__update_account_status_cache(MigTestCase): + """Coverage of accountstate update_account_status_cache function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for status cache tests.""" + configuration = self.configuration + # Ensure the mig_system_run sub directory exists for status marks + marks_path = os.path.join(configuration.mig_system_run, + status_marks_dir) + ensure_dirs_exist(marks_path) + + def test_update_account_status_cache_creates_mark(self): + """Test that update_account_status_cache creates a status mark.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": TEST_STATUS_ACTIVE, + } + + result = update_account_status_cache(configuration, user_dict) + self.assertTrue(result) + + # Verify the mark was created + cached_status = get_account_status_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_status, TEST_STATUS_ACTIVE) + + def test_update_account_status_cache_with_different_status(self): + """Test that update_account_status_cache handles different status values.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": TEST_STATUS_LOCKED, + } + + result = update_account_status_cache(configuration, user_dict) + self.assertTrue(result) + + # Verify the mark was created with correct status + cached_status = get_account_status_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_status, TEST_STATUS_LOCKED) + + def test_update_account_status_cache_with_missing_client_id(self): + """Test that update fails gracefully when client_id is missing.""" + configuration = self.configuration + user_dict = { + "status": TEST_STATUS_ACTIVE, + } + + with self.assertLogs(level='ERROR') as log_capture: + result = update_account_status_cache(configuration, user_dict) + self.assertFalse(result) + self.assertTrue(any('no client ID' in msg + for msg in log_capture.output)) + + def test_update_account_status_cache_with_missing_status(self): + """Test that update returns True when status is missing.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + } + + with self.assertLogs(level='INFO') as log_capture: + result = update_account_status_cache(configuration, user_dict) + self.assertTrue(result) + self.assertTrue(any('no status set' in msg + for msg in log_capture.output)) + + def test_update_account_status_cache_with_invalid_status(self): + """Test that update fails when status is invalid.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "invalid_status", + } + + with self.assertLogs(level='ERROR') as log_capture: + result = update_account_status_cache(configuration, user_dict) + self.assertFalse(result) + self.assertTrue(any('invalid account status' in msg + for msg in log_capture.output)) + + def test_update_account_status_cache_with_delete(self): + """Test that delete=True removes the status mark.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": TEST_STATUS_ACTIVE, + } + + # First create the mark + update_account_status_cache(configuration, user_dict) + cached_status = get_account_status_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_status, TEST_STATUS_ACTIVE) + + # Then delete it + result = update_account_status_cache( + configuration, user_dict, delete=True) + self.assertTrue(result) + + # Verify the mark was removed + cached_status = get_account_status_cache(configuration, TEST_USER_DN) + self.assertIsNone(cached_status) + + def test_update_account_status_cache_with_invalid_user_dict(self): + """Test that update fails when user_dict is not a dictionary.""" + configuration = self.configuration + + with self.assertLogs(level='ERROR') as log_capture: + result = update_account_status_cache(configuration, "not_a_dict") + self.assertFalse(result) + self.assertTrue(any('invalid user_dict' in msg + for msg in log_capture.output)) + + +class TestMigSharedAccountstate__get_account_status_cache(MigTestCase): + """Coverage of accountstate get_account_status_cache function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for status cache tests.""" + configuration = self.configuration + # Ensure the mig_system_run sub directory exists for status marks + marks_path = os.path.join(configuration.mig_system_run, + status_marks_dir) + ensure_dirs_exist(marks_path) + + def test_get_account_status_cache_returns_cached_value(self): + """Test that get_account_status_cache returns cached status value.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": TEST_STATUS_ACTIVE, + } + + # Create the mark first + update_account_status_cache(configuration, user_dict) + + # Then retrieve it + result = get_account_status_cache(configuration, TEST_USER_DN) + self.assertEqual(result, TEST_STATUS_ACTIVE) + + def test_get_account_status_cache_with_missing_client_id(self): + """Test that get fails gracefully when client_id is missing.""" + configuration = self.configuration + + with self.assertLogs(level='ERROR') as log_capture: + result = get_account_status_cache(configuration, "") + self.assertFalse(result) + self.assertTrue(any('invalid client ID' in msg + for msg in log_capture.output)) + + def test_get_account_status_cache_returns_none_when_not_set(self): + """Test that get_account_status_cache returns None when status not set.""" + configuration = self.configuration + + result = get_account_status_cache(configuration, TEST_USER_DN) + self.assertIsNone(result) + + if __name__ == "__main__": testmain() From 5b0454ee41858a5fcc4faa685fb8794b6a0cd81d Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 23:20:56 +0200 Subject: [PATCH 43/61] Add unit tests for check_account_expire . --- tests/test_mig_shared_accountstate.py | 160 +++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 1 deletion(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 17c651181..08a32f7b0 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -29,6 +29,7 @@ import time import os +import unittest # Imports of the code under test from mig.shared.accountstate import ( @@ -38,7 +39,8 @@ get_account_expire_cache, reset_account_expire_cache, get_account_status_cache, - update_account_status_cache + update_account_status_cache, + check_account_expire ) # Imports required for the unit test wrapping @@ -51,6 +53,8 @@ expire_marks_dir, status_marks_dir ) +from mig.shared.useradm import _ensure_dirs_needed_for_userdb +from mig.shared.userdb import update_user_dict # Imports required for the unit tests themselves from tests.support import ( @@ -566,5 +570,159 @@ def test_get_account_status_cache_returns_none_when_not_set(self): self.assertIsNone(result) +class TestMigSharedAccountstate__check_account_expire(MigTestCase): + """Coverage of accountstate check_account_expire function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for check_account_expire.""" + configuration = self.configuration + # Ensure the mig_system_run directory exists for expire marks + marks_path = os.path.join(configuration.mig_system_run, + expire_marks_dir) + ensure_dirs_exist(marks_path) + + _ensure_dirs_needed_for_userdb(configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + self._provision_test_user(self, TEST_USER_DN) + + def test_check_account_expire_expired(self): + """Test that check_account_expire returns False for an expired account.""" + configuration = self.configuration + logger = self.logger + # Expired account + account_expire = time.time() - 400 * 24 * 3600 + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": account_expire + } + # Update expire and delete cache for user + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_expire_cache(configuration, user_dict, delete=True) + (pending, expire, _) = check_account_expire( + configuration, TEST_USER_DN) + self.assertFalse(pending) + self.assertEqual(expire, account_expire) + + def test_check_account_expire_active(self): + """Test that check_account_expire returns expire pending for an active account.""" + configuration = self.configuration + logger = self.logger + # Active account + account_expire = time.time() + 42 * 24 * 3600 + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": account_expire + } + # Update expire and cache for user + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_expire_cache(configuration, user_dict) + (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + self.assertTrue(pending) + self.assertEqual(expire, account_expire) + + @unittest.skip("TODO: init account without expire value and enable test?") + def test_check_account_expire_no_expire_field(self): + """Test that check_account_expire returns expire pending if expire is missing.""" + configuration = self.configuration + logger = self.logger + user_dict = { + "distinguished_name": TEST_USER_DN, + } + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_expire_cache(configuration, user_dict, delete=True) + (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + self.assertTrue(pending) + self.assertEqual(expire, -1) + + def test_check_account_expire_invalid_expire_type(self): + """Test that check_account_expire returns expired if expire is not a number.""" + configuration = self.configuration + logger = self.logger + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": "invalid", + } + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + with self.assertRaises(TypeError): + (pending, expire, _) = check_account_expire(configuration, + TEST_USER_DN) + self.assertFalse(pending) + self.assertEqual(expire, -42) + + def test_check_account_expire_no_user_db_entry(self): + """Test that check_account_expire returns expired if user is not in the DB.""" + configuration = self.configuration + with self.assertLogs(level='ERROR') as log_capture: + (pending, expire, _) = check_account_expire(configuration, + "nosuchuser") + self.assertFalse(pending) + self.assertEqual(expire, -42) + self.assertTrue(any('no such account:' in msg + for msg in log_capture.output)) + + def test_check_account_expire_with_cache_miss(self): + """Test that check_account_expire updates the cache if not cached.""" + configuration = self.configuration + logger = self.logger + # Active account + account_expire = time.time() + 42 * 24 * 3600 + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": account_expire + } + # Update user and delete cache + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_expire_cache(configuration, user_dict, delete=True) + (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + self.assertTrue(pending) + self.assertEqual(expire, account_expire) + cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) + self.assertEqual(cached_expire, account_expire) + + @unittest.skip("TODO: init account without expire value and enable test?") + def test_check_account_expire_with_current_time(self): + """Test that check_account_expire uses current time if expire is not set.""" + configuration = self.configuration + logger = self.logger + user_dict = { + "distinguished_name": TEST_USER_DN, + } + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + now = time.time() + self.assertTrue(pending) + self.assertTrue(expire <= now - 3) + + def test_check_account_expire_with_expired_account_and_cache(self): + """Test that check_account_expire works if the account is expired and cached.""" + configuration = self.configuration + logger = self.logger + account_expire = time.time() - 400 * 24 * 3600 + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": account_expire, + } + update_user_dict(logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_expire_cache(configuration, user_dict) + (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + self.assertFalse(pending) + self.assertEqual(expire, account_expire) + + if __name__ == "__main__": testmain() From 965db1c8776768c587df2e1f9ec2d3dedc063864 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 23:21:49 +0200 Subject: [PATCH 44/61] Reformat with `make format-python`. --- tests/test_mig_shared_accountstate.py | 226 +++++++++++++++----------- 1 file changed, 129 insertions(+), 97 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 08a32f7b0..729822a49 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -27,31 +27,31 @@ """Unit tests for the mig shared accountstate module""" -import time import os +import time import unittest # Imports of the code under test from mig.shared.accountstate import ( - default_account_valid_days, + check_account_expire, default_account_expire, - update_account_expire_cache, + default_account_valid_days, get_account_expire_cache, - reset_account_expire_cache, get_account_status_cache, + reset_account_expire_cache, + update_account_expire_cache, update_account_status_cache, - check_account_expire ) # Imports required for the unit test wrapping from mig.shared.base import client_id_dir from mig.shared.defaults import ( AUTH_CERTIFICATE, - AUTH_OPENID_V2, - AUTH_OPENID_CONNECT, AUTH_GENERIC, + AUTH_OPENID_CONNECT, + AUTH_OPENID_V2, expire_marks_dir, - status_marks_dir + status_marks_dir, ) from mig.shared.useradm import _ensure_dirs_needed_for_userdb from mig.shared.userdb import update_user_dict @@ -59,15 +59,15 @@ # Imports required for the unit tests themselves from tests.support import ( MigTestCase, - testmain, ensure_dirs_exist, + testmain, ) -from tests.support.usersupp import TEST_USER_DN, OTHER_USER_DN +from tests.support.usersupp import OTHER_USER_DN, TEST_USER_DN # Test constants TEST_EXPIRE_TIMESTAMP = 1776031200 -TEST_STATUS_ACTIVE = 'active' -TEST_STATUS_LOCKED = 'locked' +TEST_STATUS_ACTIVE = "active" +TEST_STATUS_LOCKED = "locked" TEST_USER_DIR = client_id_dir(TEST_USER_DN) OTHER_USER_DIR = client_id_dir(OTHER_USER_DN) @@ -140,8 +140,9 @@ def test_calculates_expire_from_valid_days(self): configuration = self.configuration start_time = time.time() - result = default_account_expire(configuration, AUTH_CERTIFICATE, - start_time=start_time) + result = default_account_expire( + configuration, AUTH_CERTIFICATE, start_time=start_time + ) expected = int(start_time + 365 * 24 * 60 * 60) self.assertEqual(result, expected) @@ -150,8 +151,9 @@ def test_calculates_expire_for_openid_v2(self): configuration = self.configuration start_time = time.time() - result = default_account_expire(configuration, AUTH_OPENID_V2, - start_time=start_time) + result = default_account_expire( + configuration, AUTH_OPENID_V2, start_time=start_time + ) expected = int(start_time + 30 * 24 * 60 * 60) self.assertEqual(result, expected) @@ -160,8 +162,9 @@ def test_calculates_expire_for_openid_connect(self): configuration = self.configuration start_time = time.time() - result = default_account_expire(configuration, AUTH_OPENID_CONNECT, - start_time=start_time) + result = default_account_expire( + configuration, AUTH_OPENID_CONNECT, start_time=start_time + ) expected = int(start_time + 30 * 24 * 60 * 60) self.assertEqual(result, expected) @@ -170,8 +173,9 @@ def test_calculates_expire_for_generic_auth(self): configuration = self.configuration start_time = time.time() - result = default_account_expire(configuration, AUTH_GENERIC, - start_time=start_time) + result = default_account_expire( + configuration, AUTH_GENERIC, start_time=start_time + ) expected = int(start_time + 14 * 24 * 60 * 60) self.assertEqual(result, expected) @@ -199,8 +203,9 @@ def before_each(self): """Set up test environment for expire cache tests.""" configuration = self.configuration # Ensure the mig_system_run sub directory exists for expire marks - marks_path = os.path.join(configuration.mig_system_run, - expire_marks_dir) + marks_path = os.path.join( + configuration.mig_system_run, expire_marks_dir + ) ensure_dirs_exist(marks_path) def test_update_account_expire_cache_creates_mark(self): @@ -225,11 +230,12 @@ def test_update_account_expire_cache_with_missing_client_id(self): "expire": TEST_EXPIRE_TIMESTAMP, } - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = update_account_expire_cache(configuration, user_dict) self.assertFalse(result) - self.assertTrue(any('no client ID' in msg - for msg in log_capture.output)) + self.assertTrue( + any("no client ID" in msg for msg in log_capture.output) + ) def test_update_account_expire_cache_with_missing_expire(self): """Test that update returns True when expire is missing.""" @@ -238,11 +244,12 @@ def test_update_account_expire_cache_with_missing_expire(self): "distinguished_name": TEST_USER_DN, } - with self.assertLogs(level='INFO') as log_capture: + with self.assertLogs(level="INFO") as log_capture: result = update_account_expire_cache(configuration, user_dict) self.assertTrue(result) - self.assertTrue(any('no expire set' in msg - for msg in log_capture.output)) + self.assertTrue( + any("no expire set" in msg for msg in log_capture.output) + ) def test_update_account_expire_cache_with_string_expire(self): """Test that update fails when expire is a string.""" @@ -252,11 +259,12 @@ def test_update_account_expire_cache_with_string_expire(self): "expire": "not_a_number", } - with self.assertLogs(level='WARNING') as log_capture: + with self.assertLogs(level="WARNING") as log_capture: result = update_account_expire_cache(configuration, user_dict) self.assertFalse(result) - self.assertTrue(any('string expire value' in msg - for msg in log_capture.output)) + self.assertTrue( + any("string expire value" in msg for msg in log_capture.output) + ) def test_update_account_expire_cache_with_delete(self): """Test that delete=True removes the expire mark.""" @@ -273,7 +281,8 @@ def test_update_account_expire_cache_with_delete(self): # Then delete it result = update_account_expire_cache( - configuration, user_dict, delete=True) + configuration, user_dict, delete=True + ) self.assertTrue(result) # Verify the mark was removed @@ -284,11 +293,12 @@ def test_update_account_expire_cache_with_invalid_user_dict(self): """Test that update fails when user_dict is not a dictionary.""" configuration = self.configuration - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = update_account_expire_cache(configuration, "not_a_dict") self.assertFalse(result) - self.assertTrue(any('invalid user_dict' in msg - for msg in log_capture.output)) + self.assertTrue( + any("invalid user_dict" in msg for msg in log_capture.output) + ) class TestMigSharedAccountstate__get_account_expire_cache(MigTestCase): @@ -301,8 +311,9 @@ def before_each(self): """Set up test environment for expire cache tests.""" configuration = self.configuration # Ensure the mig_system_run sub directory exists for expire marks - marks_path = os.path.join(configuration.mig_system_run, - expire_marks_dir) + marks_path = os.path.join( + configuration.mig_system_run, expire_marks_dir + ) ensure_dirs_exist(marks_path) def test_get_account_expire_cache_returns_cached_value(self): @@ -324,11 +335,12 @@ def test_get_account_expire_cache_with_missing_client_id(self): """Test that get fails gracefully when client_id is missing.""" configuration = self.configuration - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = get_account_expire_cache(configuration, "") self.assertFalse(result) - self.assertTrue(any('invalid client ID' in msg - for msg in log_capture.output)) + self.assertTrue( + any("invalid client ID" in msg for msg in log_capture.output) + ) class TestMigSharedAccountstate__reset_account_expire_cache(MigTestCase): @@ -341,12 +353,14 @@ def before_each(self): """Set up test environment for expire cache tests.""" configuration = self.configuration # Ensure the mig_system_run sub directory exists for expire marks - marks_path = os.path.join(configuration.mig_system_run, - expire_marks_dir) + marks_path = os.path.join( + configuration.mig_system_run, expire_marks_dir + ) ensure_dirs_exist(marks_path) - self.expire_base = os.path.join(configuration.mig_system_run, - expire_marks_dir) + self.expire_base = os.path.join( + configuration.mig_system_run, expire_marks_dir + ) def test_reset_account_expire_cache_resets_all_marks(self): """Test that reset_account_expire_cache resets all expire marks.""" @@ -416,8 +430,9 @@ def before_each(self): """Set up test environment for status cache tests.""" configuration = self.configuration # Ensure the mig_system_run sub directory exists for status marks - marks_path = os.path.join(configuration.mig_system_run, - status_marks_dir) + marks_path = os.path.join( + configuration.mig_system_run, status_marks_dir + ) ensure_dirs_exist(marks_path) def test_update_account_status_cache_creates_mark(self): @@ -457,11 +472,12 @@ def test_update_account_status_cache_with_missing_client_id(self): "status": TEST_STATUS_ACTIVE, } - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = update_account_status_cache(configuration, user_dict) self.assertFalse(result) - self.assertTrue(any('no client ID' in msg - for msg in log_capture.output)) + self.assertTrue( + any("no client ID" in msg for msg in log_capture.output) + ) def test_update_account_status_cache_with_missing_status(self): """Test that update returns True when status is missing.""" @@ -470,11 +486,12 @@ def test_update_account_status_cache_with_missing_status(self): "distinguished_name": TEST_USER_DN, } - with self.assertLogs(level='INFO') as log_capture: + with self.assertLogs(level="INFO") as log_capture: result = update_account_status_cache(configuration, user_dict) self.assertTrue(result) - self.assertTrue(any('no status set' in msg - for msg in log_capture.output)) + self.assertTrue( + any("no status set" in msg for msg in log_capture.output) + ) def test_update_account_status_cache_with_invalid_status(self): """Test that update fails when status is invalid.""" @@ -484,11 +501,12 @@ def test_update_account_status_cache_with_invalid_status(self): "status": "invalid_status", } - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = update_account_status_cache(configuration, user_dict) self.assertFalse(result) - self.assertTrue(any('invalid account status' in msg - for msg in log_capture.output)) + self.assertTrue( + any("invalid account status" in msg for msg in log_capture.output) + ) def test_update_account_status_cache_with_delete(self): """Test that delete=True removes the status mark.""" @@ -505,7 +523,8 @@ def test_update_account_status_cache_with_delete(self): # Then delete it result = update_account_status_cache( - configuration, user_dict, delete=True) + configuration, user_dict, delete=True + ) self.assertTrue(result) # Verify the mark was removed @@ -516,11 +535,12 @@ def test_update_account_status_cache_with_invalid_user_dict(self): """Test that update fails when user_dict is not a dictionary.""" configuration = self.configuration - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = update_account_status_cache(configuration, "not_a_dict") self.assertFalse(result) - self.assertTrue(any('invalid user_dict' in msg - for msg in log_capture.output)) + self.assertTrue( + any("invalid user_dict" in msg for msg in log_capture.output) + ) class TestMigSharedAccountstate__get_account_status_cache(MigTestCase): @@ -533,8 +553,9 @@ def before_each(self): """Set up test environment for status cache tests.""" configuration = self.configuration # Ensure the mig_system_run sub directory exists for status marks - marks_path = os.path.join(configuration.mig_system_run, - status_marks_dir) + marks_path = os.path.join( + configuration.mig_system_run, status_marks_dir + ) ensure_dirs_exist(marks_path) def test_get_account_status_cache_returns_cached_value(self): @@ -556,11 +577,12 @@ def test_get_account_status_cache_with_missing_client_id(self): """Test that get fails gracefully when client_id is missing.""" configuration = self.configuration - with self.assertLogs(level='ERROR') as log_capture: + with self.assertLogs(level="ERROR") as log_capture: result = get_account_status_cache(configuration, "") self.assertFalse(result) - self.assertTrue(any('invalid client ID' in msg - for msg in log_capture.output)) + self.assertTrue( + any("invalid client ID" in msg for msg in log_capture.output) + ) def test_get_account_status_cache_returns_none_when_not_set(self): """Test that get_account_status_cache returns None when status not set.""" @@ -580,8 +602,9 @@ def before_each(self): """Set up test environment for check_account_expire.""" configuration = self.configuration # Ensure the mig_system_run directory exists for expire marks - marks_path = os.path.join(configuration.mig_system_run, - expire_marks_dir) + marks_path = os.path.join( + configuration.mig_system_run, expire_marks_dir + ) ensure_dirs_exist(marks_path) _ensure_dirs_needed_for_userdb(configuration) @@ -601,14 +624,14 @@ def test_check_account_expire_expired(self): account_expire = time.time() - 400 * 24 * 3600 user_dict = { "distinguished_name": TEST_USER_DN, - "expire": account_expire + "expire": account_expire, } # Update expire and delete cache for user - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_expire_cache(configuration, user_dict, delete=True) - (pending, expire, _) = check_account_expire( - configuration, TEST_USER_DN) + pending, expire, _ = check_account_expire(configuration, TEST_USER_DN) self.assertFalse(pending) self.assertEqual(expire, account_expire) @@ -620,13 +643,14 @@ def test_check_account_expire_active(self): account_expire = time.time() + 42 * 24 * 3600 user_dict = { "distinguished_name": TEST_USER_DN, - "expire": account_expire + "expire": account_expire, } # Update expire and cache for user - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_expire_cache(configuration, user_dict) - (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + pending, expire, _ = check_account_expire(configuration, TEST_USER_DN) self.assertTrue(pending) self.assertEqual(expire, account_expire) @@ -638,10 +662,11 @@ def test_check_account_expire_no_expire_field(self): user_dict = { "distinguished_name": TEST_USER_DN, } - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_expire_cache(configuration, user_dict, delete=True) - (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + pending, expire, _ = check_account_expire(configuration, TEST_USER_DN) self.assertTrue(pending) self.assertEqual(expire, -1) @@ -653,24 +678,28 @@ def test_check_account_expire_invalid_expire_type(self): "distinguished_name": TEST_USER_DN, "expire": "invalid", } - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) with self.assertRaises(TypeError): - (pending, expire, _) = check_account_expire(configuration, - TEST_USER_DN) + pending, expire, _ = check_account_expire( + configuration, TEST_USER_DN + ) self.assertFalse(pending) self.assertEqual(expire, -42) def test_check_account_expire_no_user_db_entry(self): """Test that check_account_expire returns expired if user is not in the DB.""" configuration = self.configuration - with self.assertLogs(level='ERROR') as log_capture: - (pending, expire, _) = check_account_expire(configuration, - "nosuchuser") + with self.assertLogs(level="ERROR") as log_capture: + pending, expire, _ = check_account_expire( + configuration, "nosuchuser" + ) self.assertFalse(pending) self.assertEqual(expire, -42) - self.assertTrue(any('no such account:' in msg - for msg in log_capture.output)) + self.assertTrue( + any("no such account:" in msg for msg in log_capture.output) + ) def test_check_account_expire_with_cache_miss(self): """Test that check_account_expire updates the cache if not cached.""" @@ -680,13 +709,14 @@ def test_check_account_expire_with_cache_miss(self): account_expire = time.time() + 42 * 24 * 3600 user_dict = { "distinguished_name": TEST_USER_DN, - "expire": account_expire + "expire": account_expire, } # Update user and delete cache - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_expire_cache(configuration, user_dict, delete=True) - (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + pending, expire, _ = check_account_expire(configuration, TEST_USER_DN) self.assertTrue(pending) self.assertEqual(expire, account_expire) cached_expire = get_account_expire_cache(configuration, TEST_USER_DN) @@ -700,9 +730,10 @@ def test_check_account_expire_with_current_time(self): user_dict = { "distinguished_name": TEST_USER_DN, } - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) - (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + pending, expire, _ = check_account_expire(configuration, TEST_USER_DN) now = time.time() self.assertTrue(pending) self.assertTrue(expire <= now - 3) @@ -716,10 +747,11 @@ def test_check_account_expire_with_expired_account_and_cache(self): "distinguished_name": TEST_USER_DN, "expire": account_expire, } - update_user_dict(logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_expire_cache(configuration, user_dict) - (pending, expire, _) = check_account_expire(configuration, TEST_USER_DN) + pending, expire, _ = check_account_expire(configuration, TEST_USER_DN) self.assertFalse(pending) self.assertEqual(expire, account_expire) From 761086594d0092f757ce2115470ed035e99d07cf Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 23:35:51 +0200 Subject: [PATCH 45/61] Fix test naming for default_account_valid_days and default_account_expire and adjust test without start_time to be more robust against rounding issues. --- tests/test_mig_shared_accountstate.py | 28 ++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 729822a49..9dceb9d22 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -86,35 +86,35 @@ def before_each(self): configuration.oidc_valid_days = 30 configuration.generic_valid_days = 14 - def test_returns_cert_valid_days_for_certificate_auth(self): + def test_default_account_valid_days_auth_cert(self): """Test that cert_valid_days is returned for AUTH_CERTIFICATE.""" configuration = self.configuration result = default_account_valid_days(configuration, AUTH_CERTIFICATE) self.assertEqual(result, 365) - def test_returns_oid_valid_days_for_openid_v2_auth(self): + def test_default_account_valid_days_auth_oid(self): """Test that oid_valid_days is returned for AUTH_OPENID_V2.""" configuration = self.configuration result = default_account_valid_days(configuration, AUTH_OPENID_V2) self.assertEqual(result, 30) - def test_returns_oidc_valid_days_for_openid_connect_auth(self): + def test_default_account_valid_days_auth_oidc(self): """Test that oidc_valid_days is returned for AUTH_OPENID_CONNECT.""" configuration = self.configuration result = default_account_valid_days(configuration, AUTH_OPENID_CONNECT) self.assertEqual(result, 30) - def test_returns_generic_valid_days_for_generic_auth(self): + def test_default_account_valid_days_auth_generic(self): """Test that generic_valid_days is returned for AUTH_GENERIC.""" configuration = self.configuration result = default_account_valid_days(configuration, AUTH_GENERIC) self.assertEqual(result, 14) - def test_returns_generic_valid_days_for_unknown_auth_type(self): + def test_default_account_valid_days_auth_unknown(self): """Test that generic_valid_days is fallback for unknown auth type.""" configuration = self.configuration @@ -135,8 +135,8 @@ def before_each(self): configuration.oidc_valid_days = 30 configuration.generic_valid_days = 14 - def test_calculates_expire_from_valid_days(self): - """Test that expire is calculated correctly from valid days.""" + def test_default_account_expire_for_auth_cert(self): + """Test expire calculation for AUTH_CERTIFICATE.""" configuration = self.configuration start_time = time.time() @@ -146,7 +146,7 @@ def test_calculates_expire_from_valid_days(self): expected = int(start_time + 365 * 24 * 60 * 60) self.assertEqual(result, expected) - def test_calculates_expire_for_openid_v2(self): + def test_default_account_expire_for_auth_oid(self): """Test expire calculation for AUTH_OPENID_V2.""" configuration = self.configuration @@ -157,7 +157,7 @@ def test_calculates_expire_for_openid_v2(self): expected = int(start_time + 30 * 24 * 60 * 60) self.assertEqual(result, expected) - def test_calculates_expire_for_openid_connect(self): + def test_default_account_expire_for_auth_oidc(self): """Test expire calculation for AUTH_OPENID_CONNECT.""" configuration = self.configuration @@ -168,7 +168,7 @@ def test_calculates_expire_for_openid_connect(self): expected = int(start_time + 30 * 24 * 60 * 60) self.assertEqual(result, expected) - def test_calculates_expire_for_generic_auth(self): + def test_default_account_expire_for_auth_generic(self): """Test expire calculation for AUTH_GENERIC.""" configuration = self.configuration @@ -179,7 +179,7 @@ def test_calculates_expire_for_generic_auth(self): expected = int(start_time + 14 * 24 * 60 * 60) self.assertEqual(result, expected) - def test_uses_current_time_if_start_time_not_provided(self): + def test_default_account_expire_without_start_time(self): """Test that current time is used when start_time is not provided.""" configuration = self.configuration @@ -187,9 +187,11 @@ def test_uses_current_time_if_start_time_not_provided(self): result = default_account_expire(configuration, AUTH_CERTIFICATE) after = int(time.time()) - # Result should be between before + 365 days and after + 365 days - min_expected = before + 365 * 24 * 60 * 60 + # Result should be between before + 364 days and after + 365 days + min_expected = before + 364 * 24 * 60 * 60 max_expected = after + 365 * 24 * 60 * 60 + print("DEBUG: before %s, result %s , after %s" % + (min_expected, result, max_expected)) self.assertTrue(min_expected <= result <= max_expected) From 00280f5cd4ee58aad3cc853b8a635d68b5f2f7c3 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Mon, 15 Jun 2026 23:38:50 +0200 Subject: [PATCH 46/61] Remove debug message. --- tests/test_mig_shared_accountstate.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 9dceb9d22..beb8a30e6 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -190,8 +190,6 @@ def test_default_account_expire_without_start_time(self): # Result should be between before + 364 days and after + 365 days min_expected = before + 364 * 24 * 60 * 60 max_expected = after + 365 * 24 * 60 * 60 - print("DEBUG: before %s, result %s , after %s" % - (min_expected, result, max_expected)) self.assertTrue(min_expected <= result <= max_expected) From 71c0f7b74fcf0fc4438ea8238dcdc0ab0182dc11 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 16 Jun 2026 15:17:49 +0200 Subject: [PATCH 47/61] Pull in minor fixes from #575. --- tests/test_mig_shared_griddaemons_login.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_mig_shared_griddaemons_login.py b/tests/test_mig_shared_griddaemons_login.py index 4431cc20e..4029400b8 100644 --- a/tests/test_mig_shared_griddaemons_login.py +++ b/tests/test_mig_shared_griddaemons_login.py @@ -1062,7 +1062,7 @@ def test_user_exists_with_multiple_credentials(self): """Verify login_map_lookup returns all credentials for a user.""" # Create two Login objects for 'user1' cred1 = Login( - configuration=self.configuration.daemon_conf, + configuration=self.configuration, username="user1", home="home1", password=None, @@ -1801,8 +1801,8 @@ def test_refresh_share_creds_no_change_on_unchanged_link(self): self.assertEqual(len(changed_shares), 0) self.assertEqual(len(updated_conf["shares"]), 1) - def test_refresh_share_creds_detects_missing_link(self): - """Test that a missing share link is reported as a change""" + def test_refresh_share_creds_detects_and_ignores_missing_link(self): + """Test that a completely missing share link is just ignored""" # No symlink created - share link is missing missing_share_id = "missing123" From 21c869d9590ba3f001c33cc1270d7806214c6082 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 16 Jun 2026 15:36:32 +0200 Subject: [PATCH 48/61] Pull in fix from #576. --- tests/test_mig_shared_useradm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 3e634913b..11c3284ac 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -981,7 +981,7 @@ def test_user_deletion_removes_fs_entries(self): except Exception: self.assertFalse(True, "should not be reached") - home_dir = os.path.join(self.configuration.user_home, TEST_USER_DN) + home_dir = os.path.join(self.configuration.user_home, TEST_USER_DIR) self.assertFalse(os.path.isdir(home_dir)) self.assertFalse(os.path.exists(home_dir)) short_link = os.path.join(self.configuration.user_home, @@ -993,7 +993,7 @@ def test_user_deletion_removes_fs_entries(self): TEST_USER_DIR) self.assertFalse(os.path.isdir(settings_dir)) self.assertFalse(os.path.exists(settings_dir)) - + ssh_dir = os.path.join(home_dir, ssh_conf_dir) self.assertFalse(os.path.isdir(ssh_dir)) self.assertFalse(os.path.exists(ssh_dir)) From cb724714e696537b4250c52a2819bed4942ca261 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 16 Jun 2026 16:34:47 +0200 Subject: [PATCH 49/61] Fix various merge and rebase fallout. --- tests/test_mig_shared_griddaemons_login.py | 5 +- tests/test_mig_shared_useradm.py | 227 +-------------------- 2 files changed, 2 insertions(+), 230 deletions(-) diff --git a/tests/test_mig_shared_griddaemons_login.py b/tests/test_mig_shared_griddaemons_login.py index 4029400b8..284154428 100644 --- a/tests/test_mig_shared_griddaemons_login.py +++ b/tests/test_mig_shared_griddaemons_login.py @@ -38,10 +38,6 @@ # Imports required for the unit test wrapping from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( - READ_WRITE_ACCESS, - READ_ONLY_ACCESS, - WRITE_ONLY_ACCESS, - keyword_auto READ_ONLY_ACCESS, READ_WRITE_ACCESS, UUID_USER_ID_FORMAT, @@ -598,6 +594,7 @@ def before_each(self): # Force X509 user id format self.configuration.site_user_id_format = X509_USER_ID_FORMAT + # Ensure required directories exist _ensure_dirs_needed_for_userdb(self.configuration) ensure_dirs_exist(self.configuration.sharelink_home) diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 11c3284ac..7822fc914 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -27,6 +27,7 @@ """Unit tests for the migrid module pointed to in the filename""" +import binascii import datetime import io import os @@ -84,7 +85,6 @@ # TODO: add this client dir in usersupp and import from there instead TEST_USER_DIR = TEST_USER_DN.replace('/', '+').replace(' ', '_') -TEST_USER_SHORT_ID = "abc123@some.org" TEST_USER_UUID = "UniqueUserIdForTestUser" TEST_USER_SHORT_ID = "abc123@some.org" TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] @@ -1989,231 +1989,6 @@ def test_get_any_oid_user_dn_user_check_false(self): ) self.assertEqual(result, raw_login) -class TestMigSharedUseradm__get_any_oid_user_dn( - MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin -): - """Unit tests for get_any_oid_user_dn with default user ID format.""" - - def before_each(self): - """Prepare a minimal configuration for the tests.""" - configuration = self.configuration - # Use Default format for these tests - configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT - _ensure_dirs_needed_for_userdb(self.configuration) - self.expected_user_db_home = os.path.normpath( - configuration.user_db_home - ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" - ) - ensure_dirs_exist(self.configuration.mig_system_files) - - def _provide_configuration(self): - return "testconfig" - - def _flush_test_user(self, client_id): - """Helper to force clean up after provisioned test users""" - try: - delete_user( - {'distinguished_name': client_id}, - self.configuration, - keyword_auto, - force=True, - ) - except Exception: - pass - - def test_get_any_oid_user_dn_via_alias_link(self): - """Return the distinguished name when a valid alias link exists.""" - client_id = TEST_USER_DN - short_id = TEST_USER_EMAIL - self._provision_test_user(self, client_id) - - # Make sure alias link is in place - alias_link = os.path.join(self.configuration.user_home, short_id) - client_dir = client_id_dir(client_id) - os.symlink(client_dir, alias_link) - - # Call the function – it should resolve the alias to the client_id. - result = get_any_oid_user_dn(self.configuration, raw_login=short_id, - user_check=True, do_lock=True - ) - self.assertEqual(result, client_id) - self._flush_test_user(TEST_USER_DN) - - def test_get_any_oid_user_dn_not_found(self): - """When no alias or reverse link exists, return an empty string.""" - # Missing user will cause log error - with self.assertLogs(level='ERROR') as log_capture: - result = get_any_oid_user_dn(self.configuration, - raw_login="NoSuchUser", - user_check=True, do_lock=True - ) - self.assertEqual(result, "") - self.assertTrue(any('no such openid user' in msg - for msg in log_capture.output)) - - def test_get_any_oid_user_dn_direct_dn(self): - """Return the distinguished name when a matching cert directory exists.""" - client_id = TEST_USER_DN - self._provision_test_user(self, client_id) - - # The function should recognise the directory and return the client_id. - result = get_any_oid_user_dn(self.configuration, - raw_login=TEST_USER_DN, - user_check=True, do_lock=True - ) - self.assertEqual(result, client_id) - self._flush_test_user(TEST_USER_DN) - - def test_get_any_oid_user_dn_user_check_false(self): - """When user_check=False the function bypasses the user‑dir lookup.""" - raw_login = TEST_USER_SHORT_ID - result = get_any_oid_user_dn(self.configuration, raw_login=raw_login, - user_check=False, do_lock=True - ) - self.assertEqual(result, raw_login) - -# TODO: consider merging the tests for UUID and X509 format - - -class TestMigSharedUseradm__get_any_oid_user_dn_uuid_user_id( - MigTestCase, FixtureAssertMixin, PickleAssertMixin, UserAssertMixin -): - """Unit tests for get_any_oid_user_dn with UUID user ID format.""" - - def before_each(self): - """Prepare a minimal configuration for the tests.""" - configuration = self.configuration - # Use UUID format for the tests – the function works with both UUID and X509. - configuration.site_user_id_format = UUID_USER_ID_FORMAT - _ensure_dirs_needed_for_userdb(self.configuration) - self.expected_user_db_home = os.path.normpath( - configuration.user_db_home - ) - self.expected_user_db_file = os.path.join( - self.expected_user_db_home, "MiG-users.db" - ) - ensure_dirs_exist(self.configuration.mig_system_files) - - def _provide_configuration(self): - return "testconfig" - - def _flush_test_user(self, client_id): - """Helper to force clean up after provisioned test users""" - try: - delete_user( - {'distinguished_name': client_id}, - self.configuration, - keyword_auto, - force=True, - ) - except Exception: - pass - - def test_get_any_oid_user_dn_via_lookup_link(self): - """Return the distinguished name when a valid lookup link exists.""" - client_id = TEST_USER_DN - user_dict = _provision_uuid_test_user(self.configuration, client_id) - user_id = user_dict['unique_id'] - short_id = user_dict['short_id'] - - # Make sure direct lookup link is in place - lookup_link = os.path.join(self.configuration.mig_system_run, - user_id_alias_dir, user_id) - self.assertTrue(os.path.islink(lookup_link)) - - # Call the function – it should resolve the lookup to the client_id. - result = get_any_oid_user_dn(self.configuration, raw_login=short_id, - user_check=True, do_lock=True - ) - self._flush_test_user(TEST_USER_DN) - self.assertEqual(result, client_id) - - def test_get_any_oid_user_dn_via_fallback_id_link(self): - """Return the distinguished name when a valid fallback ID link exists.""" - client_id = TEST_USER_DN - user_dict = _provision_uuid_test_user(self.configuration, client_id) - user_id = user_dict['unique_id'] - short_id = user_dict['short_id'] - client_dir = client_id_dir(client_id) - - # Blow away direct lookup link to force alias lookup - lookup_link = os.path.join(self.configuration.mig_system_run, - user_id_alias_dir, user_id) - if os.path.islink(lookup_link): - os.remove(lookup_link) - self.assertFalse(os.path.islink(lookup_link)) - - # Make sure short alias, DN link and id dir are all in place - alias_link = os.path.join(self.configuration.user_home, short_id) - self.assertTrue(os.path.islink(alias_link)) - self.assertEqual(os.path.basename(os.path.realpath(alias_link)), - user_id) - dn_link = os.path.join(self.configuration.user_home, client_dir) - self.assertTrue(os.path.islink(dn_link)) - self.assertEqual(os.path.basename(os.path.realpath(dn_link)), - user_id) - user_dir = os.path.join(self.configuration.user_home, user_id) - self.assertTrue(os.path.isdir(user_dir)) - - # Call the function – it should resolve the alias to the client_id. - result = get_any_oid_user_dn(self.configuration, raw_login=short_id, - user_check=True, do_lock=True - ) - self._flush_test_user(TEST_USER_DN) - self.assertEqual(result, client_id) - - def test_get_any_oid_user_dn_not_found(self): - """When no alias or reverse link exists, return an empty string.""" - # Missing user will cause log error - with self.assertLogs(level='ERROR') as log_capture: - result = get_any_oid_user_dn(self.configuration, - raw_login="NoSuchUser", - user_check=True, do_lock=True - ) - self.assertEqual(result, "") - self.assertTrue(any('no such openid user' in msg - for msg in log_capture.output)) - - def test_get_any_oid_user_id_direct_dn(self): - """Return the distinguished name when a matching id directory exists.""" - client_id = TEST_USER_DN - user_dict = _provision_uuid_test_user(self.configuration, client_id) - user_id = user_dict['unique_id'] - short_id = user_dict['short_id'] - client_dir = client_id_dir(client_id) - - # Make sure no lookups links get in the way and that id dir is in place - lookup_link = os.path.join(self.configuration.mig_system_run, - user_id_alias_dir, user_id) - os.remove(lookup_link) - self.assertFalse(os.path.islink(lookup_link)) - alias_link = os.path.join(self.configuration.user_home, short_id) - os.remove(alias_link) - self.assertFalse(os.path.islink(alias_link)) - reverse_link = os.path.join(self.configuration.user_home, client_dir) - os.remove(reverse_link) - self.assertFalse(os.path.islink(reverse_link)) - user_dir = os.path.join(self.configuration.user_home, user_id) - self.assertTrue(os.path.isdir(user_dir)) - - # The function should recognise the directory and return the client_id. - result = get_any_oid_user_dn(self.configuration, - raw_login=user_id, - user_check=True, do_lock=True - ) - self._flush_test_user(TEST_USER_DN) - self.assertEqual(result, user_id) - - def test_get_any_oid_user_dn_user_check_false(self): - """When user_check=False the function bypasses the user‑dir lookup.""" - raw_login = TEST_USER_SHORT_ID - result = get_any_oid_user_dn(self.configuration, raw_login=raw_login, - user_check=False, do_lock=True - ) - self.assertEqual(result, raw_login) - if __name__ == "__main__": testmain() From cffc38f3e0aec81cfcf0f3b1031c614713d89cdf Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 16 Jun 2026 16:35:26 +0200 Subject: [PATCH 50/61] Reformat with make format-python . --- tests/test_mig_shared_griddaemons_login.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/test_mig_shared_griddaemons_login.py b/tests/test_mig_shared_griddaemons_login.py index 284154428..833cff92a 100644 --- a/tests/test_mig_shared_griddaemons_login.py +++ b/tests/test_mig_shared_griddaemons_login.py @@ -66,10 +66,7 @@ ) # More imports required for the unit test wrapping -from mig.shared.useradm import ( - _ensure_dirs_needed_for_userdb, - create_user -) +from mig.shared.useradm import _ensure_dirs_needed_for_userdb, create_user # Imports required for the unit tests themselves from tests.support import ( @@ -2474,8 +2471,10 @@ def test_update_login_map_jupyter(self): user_dict=None, ) # Populate jupyter_mounts list - self.configuration.daemon_conf['jupyter_mounts'] = [ - test_session, other_session] + self.configuration.daemon_conf["jupyter_mounts"] = [ + test_session, + other_session, + ] # Call the function under test update_login_map( daemon_conf=self.configuration.daemon_conf, From e745d9d1849acecd81e89620bdb3bd72ca4522c9 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 18 Jun 2026 17:18:10 +0200 Subject: [PATCH 51/61] Add unit tests for account_expire_info and detect_special_login. --- tests/test_mig_shared_accountstate.py | 350 +++++++++++++++++++++++++- 1 file changed, 349 insertions(+), 1 deletion(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index beb8a30e6..c455580a6 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -33,9 +33,11 @@ # Imports of the code under test from mig.shared.accountstate import ( + account_expire_info, check_account_expire, default_account_expire, default_account_valid_days, + detect_special_login, get_account_expire_cache, get_account_status_cache, reset_account_expire_cache, @@ -52,6 +54,9 @@ AUTH_OPENID_V2, expire_marks_dir, status_marks_dir, + oid_auto_extend_days, + oidc_auto_extend_days, + cert_auto_extend_days, ) from mig.shared.useradm import _ensure_dirs_needed_for_userdb from mig.shared.userdb import update_user_dict @@ -68,7 +73,12 @@ TEST_EXPIRE_TIMESTAMP = 1776031200 TEST_STATUS_ACTIVE = "active" TEST_STATUS_LOCKED = "locked" - +TEST_RW_SHARE_ID = "klmnop4567" +TEST_JOB_ID = "0419b45ebc1dedbdcb91fa6251035a2096758f5d700e15478b27a90734454107" +TEST_JUPYTER_SESSION_ID = ( + "ohNo4ii9geeyei3Jai8aif6gae6Eebiechai3chegh0moo9NieveKu3AC8ooshuo" +) +TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] TEST_USER_DIR = client_id_dir(TEST_USER_DN) OTHER_USER_DIR = client_id_dir(OTHER_USER_DN) @@ -756,5 +766,343 @@ def test_check_account_expire_with_expired_account_and_cache(self): self.assertEqual(expire, account_expire) +class TestMigSharedAccountstate__account_expire_info(MigTestCase): + """Coverage of accountstate account_expire_info function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for account_expire_info.""" + configuration = self.configuration + # Ensure the mig_system_run directory exists for expire and status marks + marks_path = os.path.join( + configuration.mig_system_run, expire_marks_dir) + ensure_dirs_exist(marks_path) + marks_path = os.path.join( + configuration.mig_system_run, status_marks_dir) + ensure_dirs_exist(marks_path) + + _ensure_dirs_needed_for_userdb(configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + self._provision_test_user(self, TEST_USER_DN) + + # Set up configuration for auto-renew tests + configuration.auto_add_oid_user = True + configuration.auto_add_oidc_user = True + configuration.auto_add_cert_user = True + configuration.site_user_id_format = "X509" # Default format + + def test_account_expire_info_about_to_expire_oid(self): + """Test account_expire_info for OID user about to expire.""" + configuration = self.configuration + logger = self.logger + # Set expire to be within min_days_left (14 days) + expect_expire = time.time() + (10 * 24 * 3600) # 10 days from now + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expect_expire, + "status": "active", + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # Simulate OID login environment + environ = { + 'REMOTE_ADDR': '127.0.0.1', # Not localhost to allow renew + 'HTTP_USER_AGENT': 'some agent', + } + # Mock the requested_url_base to return OID URL + # We'll patch the function in the module, but for simplicity we set the config + # and rely on the function using the config's URLs + base_url = "https://oid.example.com" + configuration.migserver_https_ext_oid_url = base_url + environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + self.assertTrue(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, configuration.oid_valid_days) + self.assertEqual(extend_days, oid_auto_extend_days) + + def test_account_expire_info_not_about_to_expire(self): + """Test account_expire_info for user not about to expire.""" + configuration = self.configuration + logger = self.logger + # Set expire to be beyond min_days_left (14 days) + expect_expire = time.time() + (20 * 24 * 3600) # 20 days from now + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expect_expire, + "status": "active", + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + environ = { + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_USER_AGENT': 'some agent', + } + base_url = "https://oid.example.com" + configuration.migserver_https_ext_oid_url = base_url + environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + self.assertFalse(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, 0) + self.assertEqual(extend_days, 0) + + def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): + """Test account_expire_info for OID user about to expire but auto-renew disabled.""" + configuration = self.configuration + logger = self.logger + # Set expire to be within min_days_left (14 days) + expect_expire = time.time() + (10 * 24 * 3600) # 10 days from now + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expect_expire, + "status": "active", + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # Disable auto-renew for OID + configuration.auto_add_oid_user = False + + environ = { + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_USER_AGENT': 'some agent', + } + base_url = "https://oid.example.com" + configuration.migserver_https_ext_oid_url = base_url + environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + self.assertTrue(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, configuration.oid_valid_days) + self.assertEqual(extend_days, 0) # Because auto-renew is disabled + + def test_account_expire_info_about_to_expire_cert(self): + """Test account_expire_info for certificate user about to expire.""" + configuration = self.configuration + logger = self.logger + # Set expire to be within min_days_left (14 days) + expect_expire = time.time() + (10 * 24 * 3600) # 10 days from now + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expect_expire, + "status": "active", + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + environ = { + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_USER_AGENT': 'some agent', + } + base_url = "https://cert.example.com" + configuration.migserver_https_ext_cert_url = base_url + environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + self.assertTrue(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, configuration.cert_valid_days) + self.assertEqual(extend_days, cert_auto_extend_days) + + def test_account_expire_info_about_to_expire_oidc(self): + """Test account_expire_info for OIDC user about to expire.""" + configuration = self.configuration + logger = self.logger + # Set expire to be within min_days_left (14 days) + expect_expire = time.time() + (10 * 24 * 3600) # 10 days from now + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expect_expire, + "status": "active", + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + environ = { + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_USER_AGENT': 'some agent', + } + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oidc_url = base_url + environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + self.assertTrue(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, configuration.oidc_valid_days) + self.assertEqual(extend_days, oidc_auto_extend_days) + + def test_account_expire_info_with_different_min_days_left(self): + """Test account_expire_info with a custom min_days_left.""" + configuration = self.configuration + logger = self.logger + # Set expire to be 10 days from now + expect_expire = time.time() + (10 * 24 * 3600) + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expect_expire, + "status": "active", + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + environ = { + 'REMOTE_ADDR': '127.0.0.1', + 'HTTP_USER_AGENT': 'some agent', + } + base_url = "https://oid.example.com" + configuration.migserver_https_ext_oid_url = base_url + environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + + # Test with min_days_left=7 (so 10 days is beyond 7 -> not about to expire) + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=7 + ) + self.assertFalse(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, 0) + self.assertEqual(extend_days, 0) + + # Test with min_days_left=15 (so 10 days is within 15 -> about to expire) + expire_warn, account_expire, renew_days, extend_days = account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=15 + ) + self.assertTrue(expire_warn) + self.assertEqual(account_expire, expect_expire) + self.assertEqual(renew_days, configuration.oid_valid_days) + self.assertEqual(extend_days, oid_auto_extend_days) + + +class TestMigSharedAccountstate__detect_special_login(MigTestCase): + """Coverage of accountstate detect_special_login function.""" + + def _provide_configuration(self): + """Return a minimal configuration object for the test.""" + return "testconfig" + + def before_each(self): + """Create the directories that the function may touch.""" + configuration = self.configuration + self.configuration.site_enable_jobs = True + self.configuration.site_enable_jupyter = True + # Ensure the home directories that the function may reference exist + ensure_dirs_exist(configuration.user_home) + ensure_dirs_exist(configuration.mrsl_files_dir) + ensure_dirs_exist(configuration.resource_pending) + ensure_dirs_exist(configuration.sessid_to_mrsl_link_home) + ensure_dirs_exist(configuration.sessid_to_jupyter_mount_link_home) + + def test_special_login_sharelink_is_detected(self): + """Test that a sharelink ID is recognised as a special login.""" + configuration = self.configuration + # Create a dummy RW sharelink to user home + sharelink_path = os.path.join(configuration.sharelink_home, + 'read-write', TEST_RW_SHARE_ID) + sharelink_target = os.path.join(configuration.user_home, TEST_USER_DIR) + ensure_dirs_exist(sharelink_target) + ensure_dirs_exist(os.path.dirname(sharelink_path)) + os.symlink(sharelink_target, sharelink_path) + + # Call the function – it should return True for this special login + result = detect_special_login(configuration, TEST_RW_SHARE_ID, "sftp") + self.assertTrue(result) + + def test_special_login_job_is_detected(self): + """Test that a job ID is recognised as a special login.""" + configuration = self.configuration + # Create a dummy job link to mrsl files entry + job_target_path = os.path.join(configuration.mrsl_files_dir, + TEST_JOB_ID) + ensure_dirs_exist(job_target_path) + job_link_path = os.path.join(configuration.sessid_to_mrsl_link_home, + TEST_JOB_ID + ".mRSL") + ensure_dirs_exist(os.path.dirname(job_link_path)) + os.symlink(job_target_path, job_link_path) + + result = detect_special_login(configuration, TEST_JOB_ID, "sftp") + self.assertTrue(result) + + def test_special_login_jupyter_mount_is_detected(self): + """Test that a jupyter‑mount ID is recognised as a special login.""" + configuration = self.configuration + # Create a dummy jupyter‑mount link to user home + jupyter_link_path = os.path.join( + configuration.sessid_to_jupyter_mount_link_home, TEST_JUPYTER_SESSION_ID) + jupyter_target = os.path.join(configuration.user_home, TEST_USER_DIR) + ensure_dirs_exist(jupyter_target) + ensure_dirs_exist(os.path.dirname(jupyter_link_path)) + os.symlink(jupyter_target, jupyter_link_path) + + result = detect_special_login(configuration, TEST_JUPYTER_SESSION_ID, + "sftp") + self.assertTrue(result) + + def test_special_login_normal_user_is_not_detected(self): + """Test that a normal user DN is *not* recognised as a special login.""" + configuration = self.configuration + result = detect_special_login(configuration, TEST_USER_EMAIL, "sftp") + self.assertFalse(result) + + def test_special_login_without_proto_is_not_detected(self): + """Test that a value without a recognised protocol is not detected.""" + configuration = self.configuration + result = detect_special_login(configuration, TEST_USER_EMAIL, "") + self.assertFalse(result) + + def test_special_login_with_unknown_proto_is_not_detected(self): + """Test that an unknown protocol string is not detected.""" + configuration = self.configuration + result = detect_special_login(configuration, TEST_USER_EMAIL, + "unknown-proto") + self.assertFalse(result) + + def test_special_login_with_empty_user_is_not_detected(self): + """Test that an empty user string is not detected.""" + configuration = self.configuration + result = detect_special_login(configuration, "", "sftp") + self.assertFalse(result) + + if __name__ == "__main__": testmain() From 2b3998018604e48d22095b5f9064ac1ae2756b95 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 18 Jun 2026 19:02:36 +0200 Subject: [PATCH 52/61] Add tests for check_account_status. --- tests/test_mig_shared_accountstate.py | 130 +++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 3 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index c455580a6..6390f4433 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -35,6 +35,7 @@ from mig.shared.accountstate import ( account_expire_info, check_account_expire, + check_account_status, default_account_expire, default_account_valid_days, detect_special_login, @@ -64,6 +65,7 @@ # Imports required for the unit tests themselves from tests.support import ( MigTestCase, + UserAssertMixin, ensure_dirs_exist, testmain, ) @@ -820,9 +822,6 @@ def test_account_expire_info_about_to_expire_oid(self): 'REMOTE_ADDR': '127.0.0.1', # Not localhost to allow renew 'HTTP_USER_AGENT': 'some agent', } - # Mock the requested_url_base to return OID URL - # We'll patch the function in the module, but for simplicity we set the config - # and rely on the function using the config's URLs base_url = "https://oid.example.com" configuration.migserver_https_ext_oid_url = base_url environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url @@ -1032,6 +1031,7 @@ def before_each(self): ensure_dirs_exist(configuration.resource_pending) ensure_dirs_exist(configuration.sessid_to_mrsl_link_home) ensure_dirs_exist(configuration.sessid_to_jupyter_mount_link_home) + self._provision_test_user(self, TEST_USER_DN) def test_special_login_sharelink_is_detected(self): """Test that a sharelink ID is recognised as a special login.""" @@ -1104,5 +1104,129 @@ def test_special_login_with_empty_user_is_not_detected(self): self.assertFalse(result) +class TestMigSharedAccountstate__check_account_status( + MigTestCase, UserAssertMixin +): + """Coverage of accountstate check_account_status function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for check_account_status tests.""" + configuration = self.configuration + # Ensure the status‑marks directory exists + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) + ) + # Set up user DB and keep paths for later use + _ensure_dirs_needed_for_userdb(configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + ensure_dirs_exist(self.configuration.mig_system_files) + # Provision a known test user + self._provision_test_user(self, TEST_USER_DN) + + def test_check_account_status_active(self): + """Test that an active account is reported as accessible.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + } + # Create a DB entry for the user + update_user_dict(self.logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + # Populate the status cache + update_account_status_cache(configuration, user_dict) + + accessible, status, _ = check_account_status(configuration, + TEST_USER_DN) + self.assertTrue(accessible) + self.assertEqual(status, "active") + + def test_check_account_status_locked(self): + """Test that a locked account is reported as not accessible.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "locked", + } + update_user_dict(self.logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_status_cache(configuration, user_dict) + + accessible, status, _ = check_account_status(configuration, + TEST_USER_DN) + self.assertFalse(accessible) + self.assertEqual(status, "locked") + + def test_check_account_status_suspended(self): + """Test that a suspended account is reported as not accessible.""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "suspended", + } + update_user_dict(self.logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_status_cache(configuration, user_dict) + + accessible, status, _ = check_account_status(configuration, + TEST_USER_DN) + self.assertFalse(accessible) + self.assertEqual(status, "suspended") + + def test_check_account_status_unset_means_active(self): + """Test that an account without status is active and accessible.""" + configuration = self.configuration + expire_ts = time.time() + 42 * 24 * 3600 # expired in 42 days + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": expire_ts, + } + update_user_dict(self.logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_status_cache(configuration, user_dict) + + accessible, status, _ = check_account_status(configuration, + TEST_USER_DN) + self.assertTrue(accessible) + self.assertEqual(status, 'active') + + def test_check_account_status_unchanged_by_expire(self): + """Test that account status itself remains unchanged after expire.""" + configuration = self.configuration + expire_ts = time.time() - 42 * 24 * 3600 # expired 42 days ago + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict(self.logger, TEST_USER_DN, user_dict, + self.expected_user_db_file) + update_account_status_cache(configuration, user_dict) + + accessible, status, _ = check_account_status(configuration, + TEST_USER_DN) + self.assertTrue(accessible) + self.assertEqual(status, 'active') + + def test_check_account_status_missing_user(self): + """Test that check_account_status fails gracefully for a missing user.""" + configuration = self.configuration + with self.assertLogs(level="WARNING") as log_capture: + accessible, _, _ = check_account_status( + configuration, OTHER_USER_DN + ) + self.assertFalse(accessible) + self.assertTrue( + any("no such account" in msg for msg in log_capture.output) + ) + + if __name__ == "__main__": testmain() From 5f3bfba2ecdb992713ea0d16448b445bed571c3a Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 18 Jun 2026 19:03:15 +0200 Subject: [PATCH 53/61] Format unit tests with make format-python helper. --- tests/test_mig_shared_accountstate.py | 174 +++++++++++++++----------- 1 file changed, 104 insertions(+), 70 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 6390f4433..cbadb586e 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -53,11 +53,11 @@ AUTH_GENERIC, AUTH_OPENID_CONNECT, AUTH_OPENID_V2, + cert_auto_extend_days, expire_marks_dir, - status_marks_dir, oid_auto_extend_days, oidc_auto_extend_days, - cert_auto_extend_days, + status_marks_dir, ) from mig.shared.useradm import _ensure_dirs_needed_for_userdb from mig.shared.userdb import update_user_dict @@ -779,10 +779,12 @@ def before_each(self): configuration = self.configuration # Ensure the mig_system_run directory exists for expire and status marks marks_path = os.path.join( - configuration.mig_system_run, expire_marks_dir) + configuration.mig_system_run, expire_marks_dir + ) ensure_dirs_exist(marks_path) marks_path = os.path.join( - configuration.mig_system_run, status_marks_dir) + configuration.mig_system_run, status_marks_dir + ) ensure_dirs_exist(marks_path) _ensure_dirs_needed_for_userdb(configuration) @@ -819,15 +821,17 @@ def test_account_expire_info_about_to_expire_oid(self): # Simulate OID login environment environ = { - 'REMOTE_ADDR': '127.0.0.1', # Not localhost to allow renew - 'HTTP_USER_AGENT': 'some agent', + "REMOTE_ADDR": "127.0.0.1", # Not localhost to allow renew + "HTTP_USER_AGENT": "some agent", } base_url = "https://oid.example.com" configuration.migserver_https_ext_oid_url = base_url - environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=14 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -852,15 +856,17 @@ def test_account_expire_info_not_about_to_expire(self): update_account_status_cache(configuration, user_dict) environ = { - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_USER_AGENT': 'some agent', + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "some agent", } base_url = "https://oid.example.com" configuration.migserver_https_ext_oid_url = base_url - environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=14 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) ) self.assertFalse(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -888,15 +894,17 @@ def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): configuration.auto_add_oid_user = False environ = { - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_USER_AGENT': 'some agent', + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "some agent", } base_url = "https://oid.example.com" configuration.migserver_https_ext_oid_url = base_url - environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=14 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -921,15 +929,17 @@ def test_account_expire_info_about_to_expire_cert(self): update_account_status_cache(configuration, user_dict) environ = { - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_USER_AGENT': 'some agent', + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "some agent", } base_url = "https://cert.example.com" configuration.migserver_https_ext_cert_url = base_url - environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=14 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -954,15 +964,17 @@ def test_account_expire_info_about_to_expire_oidc(self): update_account_status_cache(configuration, user_dict) environ = { - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_USER_AGENT': 'some agent', + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "some agent", } base_url = "https://oidc.example.com" configuration.migserver_https_ext_oidc_url = base_url - environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=14 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -987,16 +999,18 @@ def test_account_expire_info_with_different_min_days_left(self): update_account_status_cache(configuration, user_dict) environ = { - 'REMOTE_ADDR': '127.0.0.1', - 'HTTP_USER_AGENT': 'some agent', + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "some agent", } base_url = "https://oid.example.com" configuration.migserver_https_ext_oid_url = base_url - environ['SCRIPT_URI'] = '%s/wsgi-bin/something.py' % base_url + environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url # Test with min_days_left=7 (so 10 days is beyond 7 -> not about to expire) - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=7 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=7 + ) ) self.assertFalse(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -1004,8 +1018,10 @@ def test_account_expire_info_with_different_min_days_left(self): self.assertEqual(extend_days, 0) # Test with min_days_left=15 (so 10 days is within 15 -> about to expire) - expire_warn, account_expire, renew_days, extend_days = account_expire_info( - configuration, TEST_USER_DN, environ, min_days_left=15 + expire_warn, account_expire, renew_days, extend_days = ( + account_expire_info( + configuration, TEST_USER_DN, environ, min_days_left=15 + ) ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) @@ -1037,8 +1053,9 @@ def test_special_login_sharelink_is_detected(self): """Test that a sharelink ID is recognised as a special login.""" configuration = self.configuration # Create a dummy RW sharelink to user home - sharelink_path = os.path.join(configuration.sharelink_home, - 'read-write', TEST_RW_SHARE_ID) + sharelink_path = os.path.join( + configuration.sharelink_home, "read-write", TEST_RW_SHARE_ID + ) sharelink_target = os.path.join(configuration.user_home, TEST_USER_DIR) ensure_dirs_exist(sharelink_target) ensure_dirs_exist(os.path.dirname(sharelink_path)) @@ -1052,11 +1069,13 @@ def test_special_login_job_is_detected(self): """Test that a job ID is recognised as a special login.""" configuration = self.configuration # Create a dummy job link to mrsl files entry - job_target_path = os.path.join(configuration.mrsl_files_dir, - TEST_JOB_ID) + job_target_path = os.path.join( + configuration.mrsl_files_dir, TEST_JOB_ID + ) ensure_dirs_exist(job_target_path) - job_link_path = os.path.join(configuration.sessid_to_mrsl_link_home, - TEST_JOB_ID + ".mRSL") + job_link_path = os.path.join( + configuration.sessid_to_mrsl_link_home, TEST_JOB_ID + ".mRSL" + ) ensure_dirs_exist(os.path.dirname(job_link_path)) os.symlink(job_target_path, job_link_path) @@ -1068,14 +1087,17 @@ def test_special_login_jupyter_mount_is_detected(self): configuration = self.configuration # Create a dummy jupyter‑mount link to user home jupyter_link_path = os.path.join( - configuration.sessid_to_jupyter_mount_link_home, TEST_JUPYTER_SESSION_ID) + configuration.sessid_to_jupyter_mount_link_home, + TEST_JUPYTER_SESSION_ID, + ) jupyter_target = os.path.join(configuration.user_home, TEST_USER_DIR) ensure_dirs_exist(jupyter_target) ensure_dirs_exist(os.path.dirname(jupyter_link_path)) os.symlink(jupyter_target, jupyter_link_path) - result = detect_special_login(configuration, TEST_JUPYTER_SESSION_ID, - "sftp") + result = detect_special_login( + configuration, TEST_JUPYTER_SESSION_ID, "sftp" + ) self.assertTrue(result) def test_special_login_normal_user_is_not_detected(self): @@ -1093,8 +1115,9 @@ def test_special_login_without_proto_is_not_detected(self): def test_special_login_with_unknown_proto_is_not_detected(self): """Test that an unknown protocol string is not detected.""" configuration = self.configuration - result = detect_special_login(configuration, TEST_USER_EMAIL, - "unknown-proto") + result = detect_special_login( + configuration, TEST_USER_EMAIL, "unknown-proto" + ) self.assertFalse(result) def test_special_login_with_empty_user_is_not_detected(self): @@ -1122,7 +1145,8 @@ def before_each(self): # Set up user DB and keep paths for later use _ensure_dirs_needed_for_userdb(configuration) self.expected_user_db_home = os.path.normpath( - configuration.user_db_home) + configuration.user_db_home + ) self.expected_user_db_file = os.path.join( self.expected_user_db_home, "MiG-users.db" ) @@ -1138,13 +1162,15 @@ def test_check_account_status_active(self): "status": "active", } # Create a DB entry for the user - update_user_dict(self.logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) # Populate the status cache update_account_status_cache(configuration, user_dict) - accessible, status, _ = check_account_status(configuration, - TEST_USER_DN) + accessible, status, _ = check_account_status( + configuration, TEST_USER_DN + ) self.assertTrue(accessible) self.assertEqual(status, "active") @@ -1155,12 +1181,14 @@ def test_check_account_status_locked(self): "distinguished_name": TEST_USER_DN, "status": "locked", } - update_user_dict(self.logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_status_cache(configuration, user_dict) - accessible, status, _ = check_account_status(configuration, - TEST_USER_DN) + accessible, status, _ = check_account_status( + configuration, TEST_USER_DN + ) self.assertFalse(accessible) self.assertEqual(status, "locked") @@ -1171,12 +1199,14 @@ def test_check_account_status_suspended(self): "distinguished_name": TEST_USER_DN, "status": "suspended", } - update_user_dict(self.logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_status_cache(configuration, user_dict) - accessible, status, _ = check_account_status(configuration, - TEST_USER_DN) + accessible, status, _ = check_account_status( + configuration, TEST_USER_DN + ) self.assertFalse(accessible) self.assertEqual(status, "suspended") @@ -1188,14 +1218,16 @@ def test_check_account_status_unset_means_active(self): "distinguished_name": TEST_USER_DN, "expire": expire_ts, } - update_user_dict(self.logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_status_cache(configuration, user_dict) - accessible, status, _ = check_account_status(configuration, - TEST_USER_DN) + accessible, status, _ = check_account_status( + configuration, TEST_USER_DN + ) self.assertTrue(accessible) - self.assertEqual(status, 'active') + self.assertEqual(status, "active") def test_check_account_status_unchanged_by_expire(self): """Test that account status itself remains unchanged after expire.""" @@ -1206,14 +1238,16 @@ def test_check_account_status_unchanged_by_expire(self): "status": "active", "expire": expire_ts, } - update_user_dict(self.logger, TEST_USER_DN, user_dict, - self.expected_user_db_file) + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) update_account_status_cache(configuration, user_dict) - accessible, status, _ = check_account_status(configuration, - TEST_USER_DN) + accessible, status, _ = check_account_status( + configuration, TEST_USER_DN + ) self.assertTrue(accessible) - self.assertEqual(status, 'active') + self.assertEqual(status, "active") def test_check_account_status_missing_user(self): """Test that check_account_status fails gracefully for a missing user.""" From c82823df87d7c69d39f0de84ab3d5e2d8b411b48 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Thu, 18 Jun 2026 20:00:03 +0200 Subject: [PATCH 54/61] Add tests for check_update_account_expire. --- tests/test_mig_shared_accountstate.py | 312 ++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index cbadb586e..cad1633f2 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -34,6 +34,7 @@ # Imports of the code under test from mig.shared.accountstate import ( account_expire_info, + check_account_accessible, check_account_expire, check_account_status, default_account_expire, @@ -1039,6 +1040,7 @@ def _provide_configuration(self): def before_each(self): """Create the directories that the function may touch.""" configuration = self.configuration + self.configuration.site_enable_sharelinks = True self.configuration.site_enable_jobs = True self.configuration.site_enable_jupyter = True # Ensure the home directories that the function may reference exist @@ -1262,5 +1264,315 @@ def test_check_account_status_missing_user(self): ) +class TestMigSharedAccountstate__check_account_accessible( + MigTestCase, UserAssertMixin +): + """Coverage of accountstate check_account_accessible function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for check_account_accessible tests.""" + configuration = self.configuration + self.configuration.site_enable_sharelinks = True + self.configuration.site_enable_jobs = True + self.configuration.site_enable_jupyter = True + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + _ensure_dirs_needed_for_userdb(configuration) + # Keep paths for possible later use + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + # Provision a known test user + self._provision_test_user(self, TEST_USER_DN) + # This simulates an existing short id link to X509 in user_home + short_id_link_in_home = os.path.join( + configuration.user_home, TEST_USER_EMAIL + ) + os.symlink(TEST_USER_DIR, short_id_link_in_home) + + def test_check_account_accessible_active_user_web_access(self): + """Test that an active user is accessible via web (non-IO).""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oidc", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_active_user_io_access(self): + """Test that an active user is accessible via SFTP (IO).""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_active_user_web_access_on_email_alias( + self, + ): + """Test that an active user is accessible via web (non-IO) using email alias.""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "oidc", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_active_user_sftp_access_on_email_alias( + self, + ): + """Test that an active user is accessible via SFTP using email alias.""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_locked_user_web_blocked(self): + """Test that a locked user is not accessible on web (non-IO).""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "locked", + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oidc", io_login=False + ) + self.assertFalse(accessible) + + def test_check_account_accessible_locked_user_io_blocked(self): + """Test that a locked user is not accessible on SFTP (IO).""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "locked", + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp" + ) + self.assertFalse(accessible) + + def test_check_account_accessible_expired_user_blocked(self): + """Test that an expired user is inaccessible on web (non-ID) login.""" + configuration = self.configuration + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oidc", io_login=False + ) + self.assertFalse(accessible) + + def test_check_account_accessible_expired_user_sftp_access_blocked(self): + """Test that an expired user is inaccessible on sftp if enenforced.""" + configuration = self.configuration + configuration.site_io_account_expire = True + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp" + ) + self.assertFalse(accessible) + + def test_check_account_accessible_special_login_sharelink_access(self): + """Test that a sharelink is detected as special login and accessible.""" + configuration = self.configuration + # Create a dummy RW sharelink to user home + sharelink_path = os.path.join( + configuration.sharelink_home, "read-write", TEST_RW_SHARE_ID + ) + sharelink_target = os.path.join(configuration.user_home, TEST_USER_DIR) + ensure_dirs_exist(sharelink_target) + ensure_dirs_exist(os.path.dirname(sharelink_path)) + os.symlink(sharelink_target, sharelink_path) + + accessible = check_account_accessible( + configuration, TEST_RW_SHARE_ID, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_special_login_job_access(self): + """Test that a job ID is detected as special login and accessible.""" + configuration = self.configuration + configuration.site_enable_jobs = True + # Create a dummy job link to mrsl files entry + job_target_path = os.path.join( + configuration.mrsl_files_dir, TEST_JOB_ID + ) + ensure_dirs_exist(job_target_path) + job_link_path = os.path.join( + configuration.sessid_to_mrsl_link_home, TEST_JOB_ID + ".mRSL" + ) + ensure_dirs_exist(os.path.dirname(job_link_path)) + os.symlink(job_target_path, job_link_path) + + accessible = check_account_accessible( + configuration, TEST_JOB_ID, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_special_login_jupyter_access(self): + """Test that a jupyter mount ID is detected as special login and accessible.""" + configuration = self.configuration + configuration.site_enable_jupyter = True + # Create a dummy jupyter‑mount link to user home + jupyter_link_path = os.path.join( + configuration.sessid_to_jupyter_mount_link_home, + TEST_JUPYTER_SESSION_ID, + ) + jupyter_target = os.path.join(configuration.user_home, TEST_USER_DIR) + ensure_dirs_exist(jupyter_target) + ensure_dirs_exist(os.path.dirname(jupyter_link_path)) + os.symlink(jupyter_target, jupyter_link_path) + + accessible = check_account_accessible( + configuration, TEST_JUPYTER_SESSION_ID, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_missing_user_web_rejected(self): + """Test that check_account_accessible rejects a missing user on web (non-IO).""" + configuration = self.configuration + accessible = check_account_accessible( + configuration, OTHER_USER_DN, "oidc", io_login=False + ) + self.assertFalse(accessible) + + def test_check_account_accessible_missing_user_io_rejected(self): + """Test that check_account_accessible rejects a missing user on SFTP (IO).""" + configuration = self.configuration + accessible = check_account_accessible( + configuration, OTHER_USER_DN, "sftp" + ) + self.assertFalse(accessible) + + def test_check_account_accessible_io_expire_disabled(self): + """Test that account remains accessible on SFTP (IO) when expire is not enforced.""" + configuration = self.configuration + configuration.site_io_account_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp", io_login=True + ) + self.assertTrue(accessible) # Because IO expire is disabled + + def test_check_account_accessible_openid_expire_disabled(self): + """Test that account remains accessible on OpenID (non-IO) when expire is not enforced.""" + configuration = self.configuration + configuration.user_openid_enforce_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oid", io_login=False + ) + self.assertTrue(accessible) # Because OpenID expire is disabled + + if __name__ == "__main__": testmain() From 15379fb5a2fcd198a5e75fad0d0889857425640c Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Fri, 19 Jun 2026 10:26:07 +0200 Subject: [PATCH 55/61] Add unit tests for check_update_account_expire and sync back some constants to other existing tests for consistency. --- tests/test_mig_shared_accountstate.py | 478 +++++++++++++++++++++----- 1 file changed, 390 insertions(+), 88 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index cad1633f2..2fc5254c5 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -37,6 +37,7 @@ check_account_accessible, check_account_expire, check_account_status, + check_update_account_expire, default_account_expire, default_account_valid_days, detect_special_login, @@ -61,7 +62,7 @@ status_marks_dir, ) from mig.shared.useradm import _ensure_dirs_needed_for_userdb -from mig.shared.userdb import update_user_dict +from mig.shared.userdb import load_user_dict, update_user_dict # Imports required for the unit tests themselves from tests.support import ( @@ -76,6 +77,8 @@ TEST_EXPIRE_TIMESTAMP = 1776031200 TEST_STATUS_ACTIVE = "active" TEST_STATUS_LOCKED = "locked" +TEST_STATUS_SUSPENDED = "suspended" +TEST_STATUS_TEMPORAL = "temporal" TEST_RW_SHARE_ID = "klmnop4567" TEST_JOB_ID = "0419b45ebc1dedbdcb91fa6251035a2096758f5d700e15478b27a90734454107" TEST_JUPYTER_SESSION_ID = ( @@ -215,11 +218,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for expire cache tests.""" configuration = self.configuration - # Ensure the mig_system_run sub directory exists for expire marks - marks_path = os.path.join( - configuration.mig_system_run, expire_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) ) - ensure_dirs_exist(marks_path) def test_update_account_expire_cache_creates_mark(self): """Test that update_account_expire_cache creates an expire mark.""" @@ -323,11 +326,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for expire cache tests.""" configuration = self.configuration - # Ensure the mig_system_run sub directory exists for expire marks - marks_path = os.path.join( - configuration.mig_system_run, expire_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) ) - ensure_dirs_exist(marks_path) def test_get_account_expire_cache_returns_cached_value(self): """Test that get_account_expire_cache returns cached expire value.""" @@ -365,12 +368,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for expire cache tests.""" configuration = self.configuration - # Ensure the mig_system_run sub directory exists for expire marks - marks_path = os.path.join( - configuration.mig_system_run, expire_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) ) - ensure_dirs_exist(marks_path) - self.expire_base = os.path.join( configuration.mig_system_run, expire_marks_dir ) @@ -442,11 +444,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for status cache tests.""" configuration = self.configuration - # Ensure the mig_system_run sub directory exists for status marks - marks_path = os.path.join( - configuration.mig_system_run, status_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) ) - ensure_dirs_exist(marks_path) def test_update_account_status_cache_creates_mark(self): """Test that update_account_status_cache creates a status mark.""" @@ -565,11 +567,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for status cache tests.""" configuration = self.configuration - # Ensure the mig_system_run sub directory exists for status marks - marks_path = os.path.join( - configuration.mig_system_run, status_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) ) - ensure_dirs_exist(marks_path) def test_get_account_status_cache_returns_cached_value(self): """Test that get_account_status_cache returns cached status value.""" @@ -614,12 +616,12 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for check_account_expire.""" configuration = self.configuration - # Ensure the mig_system_run directory exists for expire marks - marks_path = os.path.join( - configuration.mig_system_run, expire_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) ) - ensure_dirs_exist(marks_path) - + # Set up user DB and keep paths for later use _ensure_dirs_needed_for_userdb(configuration) self.expected_user_db_home = os.path.normpath( configuration.user_db_home @@ -778,16 +780,15 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for account_expire_info.""" configuration = self.configuration - # Ensure the mig_system_run directory exists for expire and status marks - marks_path = os.path.join( - configuration.mig_system_run, expire_marks_dir + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) ) - ensure_dirs_exist(marks_path) - marks_path = os.path.join( - configuration.mig_system_run, status_marks_dir + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) ) - ensure_dirs_exist(marks_path) - + # Set up user DB and keep paths for later use _ensure_dirs_needed_for_userdb(configuration) self.expected_user_db_home = os.path.normpath( configuration.user_db_home @@ -803,8 +804,8 @@ def before_each(self): configuration.auto_add_cert_user = True configuration.site_user_id_format = "X509" # Default format - def test_account_expire_info_about_to_expire_oid(self): - """Test account_expire_info for OID user about to expire.""" + def test_account_expire_info_about_to_expire_cert(self): + """Test account_expire_info for certificate user about to expire.""" configuration = self.configuration logger = self.logger # Set expire to be within min_days_left (14 days) @@ -820,13 +821,12 @@ def test_account_expire_info_about_to_expire_oid(self): update_account_expire_cache(configuration, user_dict) update_account_status_cache(configuration, user_dict) - # Simulate OID login environment environ = { - "REMOTE_ADDR": "127.0.0.1", # Not localhost to allow renew + "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": "some agent", } - base_url = "https://oid.example.com" - configuration.migserver_https_ext_oid_url = base_url + base_url = "https://cert.example.com" + configuration.migserver_https_ext_cert_url = base_url environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url expire_warn, account_expire, renew_days, extend_days = ( @@ -836,15 +836,15 @@ def test_account_expire_info_about_to_expire_oid(self): ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) - self.assertEqual(renew_days, configuration.oid_valid_days) - self.assertEqual(extend_days, oid_auto_extend_days) + self.assertEqual(renew_days, configuration.cert_valid_days) + self.assertEqual(extend_days, cert_auto_extend_days) - def test_account_expire_info_not_about_to_expire(self): - """Test account_expire_info for user not about to expire.""" + def test_account_expire_info_about_to_expire_oid(self): + """Test account_expire_info for OID user about to expire.""" configuration = self.configuration logger = self.logger - # Set expire to be beyond min_days_left (14 days) - expect_expire = time.time() + (20 * 24 * 3600) # 20 days from now + # Set expire to be within min_days_left (14 days) + expect_expire = time.time() + (10 * 24 * 3600) # 10 days from now user_dict = { "distinguished_name": TEST_USER_DN, "expire": expect_expire, @@ -856,8 +856,9 @@ def test_account_expire_info_not_about_to_expire(self): update_account_expire_cache(configuration, user_dict) update_account_status_cache(configuration, user_dict) + # Simulate OID login environment environ = { - "REMOTE_ADDR": "127.0.0.1", + "REMOTE_ADDR": "127.0.0.1", # Not localhost to allow renew "HTTP_USER_AGENT": "some agent", } base_url = "https://oid.example.com" @@ -869,13 +870,13 @@ def test_account_expire_info_not_about_to_expire(self): configuration, TEST_USER_DN, environ, min_days_left=14 ) ) - self.assertFalse(expire_warn) + self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) - self.assertEqual(renew_days, 0) - self.assertEqual(extend_days, 0) + self.assertEqual(renew_days, configuration.oid_valid_days) + self.assertEqual(extend_days, oid_auto_extend_days) - def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): - """Test account_expire_info for OID user about to expire but auto-renew disabled.""" + def test_account_expire_info_about_to_expire_oidc(self): + """Test account_expire_info for OIDC user about to expire.""" configuration = self.configuration logger = self.logger # Set expire to be within min_days_left (14 days) @@ -891,15 +892,13 @@ def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): update_account_expire_cache(configuration, user_dict) update_account_status_cache(configuration, user_dict) - # Disable auto-renew for OID - configuration.auto_add_oid_user = False - + # Simulate OIDC login environment environ = { "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": "some agent", } - base_url = "https://oid.example.com" - configuration.migserver_https_ext_oid_url = base_url + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oidc_url = base_url environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url expire_warn, account_expire, renew_days, extend_days = ( @@ -909,15 +908,15 @@ def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) - self.assertEqual(renew_days, configuration.oid_valid_days) - self.assertEqual(extend_days, 0) # Because auto-renew is disabled + self.assertEqual(renew_days, configuration.oidc_valid_days) + self.assertEqual(extend_days, oidc_auto_extend_days) - def test_account_expire_info_about_to_expire_cert(self): - """Test account_expire_info for certificate user about to expire.""" + def test_account_expire_info_not_about_to_expire(self): + """Test account_expire_info for user not about to expire.""" configuration = self.configuration logger = self.logger - # Set expire to be within min_days_left (14 days) - expect_expire = time.time() + (10 * 24 * 3600) # 10 days from now + # Set expire to be beyond min_days_left (14 days) + expect_expire = time.time() + (20 * 24 * 3600) # 20 days from now user_dict = { "distinguished_name": TEST_USER_DN, "expire": expect_expire, @@ -933,8 +932,8 @@ def test_account_expire_info_about_to_expire_cert(self): "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": "some agent", } - base_url = "https://cert.example.com" - configuration.migserver_https_ext_cert_url = base_url + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oidc_url = base_url environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url expire_warn, account_expire, renew_days, extend_days = ( @@ -942,13 +941,13 @@ def test_account_expire_info_about_to_expire_cert(self): configuration, TEST_USER_DN, environ, min_days_left=14 ) ) - self.assertTrue(expire_warn) + self.assertFalse(expire_warn) self.assertEqual(account_expire, expect_expire) - self.assertEqual(renew_days, configuration.cert_valid_days) - self.assertEqual(extend_days, cert_auto_extend_days) + self.assertEqual(renew_days, 0) + self.assertEqual(extend_days, 0) - def test_account_expire_info_about_to_expire_oidc(self): - """Test account_expire_info for OIDC user about to expire.""" + def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): + """Test account_expire_info for OID user about to expire but auto-renew disabled.""" configuration = self.configuration logger = self.logger # Set expire to be within min_days_left (14 days) @@ -964,12 +963,15 @@ def test_account_expire_info_about_to_expire_oidc(self): update_account_expire_cache(configuration, user_dict) update_account_status_cache(configuration, user_dict) + # Disable auto-renew for OID + configuration.auto_add_oid_user = False + environ = { "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": "some agent", } - base_url = "https://oidc.example.com" - configuration.migserver_https_ext_oidc_url = base_url + base_url = "https://oid.example.com" + configuration.migserver_https_ext_oid_url = base_url environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url expire_warn, account_expire, renew_days, extend_days = ( @@ -979,8 +981,8 @@ def test_account_expire_info_about_to_expire_oidc(self): ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) - self.assertEqual(renew_days, configuration.oidc_valid_days) - self.assertEqual(extend_days, oidc_auto_extend_days) + self.assertEqual(renew_days, configuration.oid_valid_days) + self.assertEqual(extend_days, 0) # Because auto-renew is disabled def test_account_expire_info_with_different_min_days_left(self): """Test account_expire_info with a custom min_days_left.""" @@ -1003,8 +1005,8 @@ def test_account_expire_info_with_different_min_days_left(self): "REMOTE_ADDR": "127.0.0.1", "HTTP_USER_AGENT": "some agent", } - base_url = "https://oid.example.com" - configuration.migserver_https_ext_oid_url = base_url + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oidc_url = base_url environ["SCRIPT_URI"] = "%s/wsgi-bin/something.py" % base_url # Test with min_days_left=7 (so 10 days is beyond 7 -> not about to expire) @@ -1026,8 +1028,8 @@ def test_account_expire_info_with_different_min_days_left(self): ) self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) - self.assertEqual(renew_days, configuration.oid_valid_days) - self.assertEqual(extend_days, oid_auto_extend_days) + self.assertEqual(renew_days, configuration.oidc_valid_days) + self.assertEqual(extend_days, oidc_auto_extend_days) class TestMigSharedAccountstate__detect_special_login(MigTestCase): @@ -1140,7 +1142,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for check_account_status tests.""" configuration = self.configuration - # Ensure the status‑marks directory exists + # Ensure necessary directories exist + ensure_dirs_exist(self.configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) + ) ensure_dirs_exist( os.path.join(configuration.mig_system_run, status_marks_dir) ) @@ -1152,7 +1158,6 @@ def before_each(self): self.expected_user_db_file = os.path.join( self.expected_user_db_home, "MiG-users.db" ) - ensure_dirs_exist(self.configuration.mig_system_files) # Provision a known test user self._provision_test_user(self, TEST_USER_DN) @@ -1181,7 +1186,7 @@ def test_check_account_status_locked(self): configuration = self.configuration user_dict = { "distinguished_name": TEST_USER_DN, - "status": "locked", + "status": TEST_STATUS_LOCKED, } update_user_dict( self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file @@ -1192,14 +1197,14 @@ def test_check_account_status_locked(self): configuration, TEST_USER_DN ) self.assertFalse(accessible) - self.assertEqual(status, "locked") + self.assertEqual(status, TEST_STATUS_LOCKED) def test_check_account_status_suspended(self): """Test that a suspended account is reported as not accessible.""" configuration = self.configuration user_dict = { "distinguished_name": TEST_USER_DN, - "status": "suspended", + "status": TEST_STATUS_SUSPENDED, } update_user_dict( self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file @@ -1210,7 +1215,7 @@ def test_check_account_status_suspended(self): configuration, TEST_USER_DN ) self.assertFalse(accessible) - self.assertEqual(status, "suspended") + self.assertEqual(status, TEST_STATUS_SUSPENDED) def test_check_account_status_unset_means_active(self): """Test that an account without status is active and accessible.""" @@ -1280,8 +1285,14 @@ def before_each(self): self.configuration.site_enable_jupyter = True # Ensure necessary directories exist ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) + ) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) + ) + # Set up user DB and keep paths for later use _ensure_dirs_needed_for_userdb(configuration) - # Keep paths for possible later use self.expected_user_db_home = os.path.normpath( configuration.user_db_home ) @@ -1385,7 +1396,7 @@ def test_check_account_accessible_locked_user_web_blocked(self): configuration = self.configuration user_dict = { "distinguished_name": TEST_USER_DN, - "status": "locked", + "status": TEST_STATUS_LOCKED, } update_user_dict( self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file @@ -1402,7 +1413,7 @@ def test_check_account_accessible_locked_user_io_blocked(self): configuration = self.configuration user_dict = { "distinguished_name": TEST_USER_DN, - "status": "locked", + "status": TEST_STATUS_LOCKED, } update_user_dict( self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file @@ -1574,5 +1585,296 @@ def test_check_account_accessible_openid_expire_disabled(self): self.assertTrue(accessible) # Because OpenID expire is disabled +class TestMigSharedAccountstate__check_update_account_expire( + MigTestCase, UserAssertMixin +): + """Coverage of accountstate check_update_account_expire function.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for check_update_account_expire tests.""" + configuration = self.configuration + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) + ) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) + ) + # Set up user DB and keep paths for later use + _ensure_dirs_needed_for_userdb(configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + self._provision_test_user(self, TEST_USER_DN) + + def _update_account_in_db_and_cache( + self, expire, status=TEST_STATUS_ACTIVE + ): + """Write account status and expire data to the DB and cache.""" + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": status, + "expire": expire, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(self.configuration, user_dict) + update_account_status_cache(self.configuration, user_dict) + return user_dict + + def test_check_update_account_expire_auto_extends_oid(self): + """Test auto extension for an OID user about to expire.""" + configuration = self.configuration + expire = time.time() + 5 * 24 * 3600 + self._update_account_in_db_and_cache(expire) + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oid_url = base_url + configuration.auto_add_oid_user = True + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertTrue(pending) + self.assertEqual(cached_expire, expire) + self.assertTrue(user_dict) + self.assertTrue(user_dict["expire"] > expire) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + user_dict["expire"], + ) + self.assertEqual( + load_user_dict( + self.logger, TEST_USER_DN, self.expected_user_db_file + )["expire"], + user_dict["expire"], + ) + + def test_check_update_account_expire_auto_extends_oidc(self): + """Test auto extension for an OIDC user about to expire.""" + configuration = self.configuration + expire = time.time() + 5 * 24 * 3600 + self._update_account_in_db_and_cache(expire) + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oidc_url = base_url + configuration.auto_add_oidc_user = True + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertTrue(pending) + self.assertEqual(cached_expire, expire) + self.assertTrue(user_dict) + self.assertTrue(user_dict["expire"] > expire) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + user_dict["expire"], + ) + + def test_check_update_account_expire_auto_extends_cert(self): + """Test auto extension for a certificate user about to expire.""" + configuration = self.configuration + expire = time.time() + 5 * 24 * 3600 + self._update_account_in_db_and_cache(expire) + base_url = "https://cert.example.com" + configuration.migserver_https_ext_cert_url = base_url + configuration.auto_add_cert_user = True + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertTrue(pending) + self.assertEqual(cached_expire, expire) + self.assertTrue(user_dict) + self.assertTrue(user_dict["expire"] > expire) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + user_dict["expire"], + ) + + def test_check_update_account_expire_not_near_expiry(self): + """Test that users not near expiry are not auto-extended.""" + configuration = self.configuration + expire = time.time() + 30 * 24 * 3600 + self._update_account_in_db_and_cache(expire) + base_url = "https://oidc.example.com" + configuration.migserver_https_ext_oidc_url = base_url + configuration.auto_add_oidc_user = True + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertTrue(pending) + self.assertEqual(cached_expire, expire) + # NOTE: for expired users with cache mark no user_dict is loaded + self.assertEqual(user_dict, None) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + expire, + ) + + def test_check_update_account_expire_no_matching_auto_update(self): + """Test that near-expiry users keep expire without matching vhost.""" + configuration = self.configuration + expire = time.time() + 5 * 24 * 3600 + self._update_account_in_db_and_cache(expire) + configuration.migserver_https_ext_oidc_url = "https://oidc.example.com" + configuration.auto_add_oidc_user = True + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "https://unknown.example.com/wsgi-bin/page.py", + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertTrue(pending) + self.assertEqual(cached_expire, expire) + # NOTE: for expired users with cache mark no user_dict is loaded + self.assertEqual(user_dict, None) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + expire, + ) + + def test_check_update_account_expire_skips_temporal_user_without_peer(self): + """Test that local (typically external) temporal users are not auto-extended without peer.""" + configuration = self.configuration + expire = 42 + self._update_account_in_db_and_cache(expire, TEST_STATUS_TEMPORAL) + base_url = "https://ext.example.com" + configuration.migserver_https_mig_oid_url = base_url + # Enable renew with peers but test without any active + configuration.auto_add_oid_user = True + self.site_enable_peers = True + self.site_peers_mandatory = True + self.site_peers_explicit_fields = ["email"] + environ = { + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertFalse(pending) + self.assertEqual(cached_expire, expire) + # NOTE: for expired users with cache mark no user_dict is loaded + self.assertEqual(user_dict, None) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + expire, + ) + + def test_check_update_account_expire_does_not_extend_locked_user(self): + """Test that locked users are not auto-extended.""" + configuration = self.configuration + expire = time.time() + 5 * 24 * 3600 + self._update_account_in_db_and_cache(expire, status=TEST_STATUS_LOCKED) + base_url = "https://oid.example.com" + configuration.migserver_https_ext_oidc_url = base_url + configuration.auto_add_oidc_user = True + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertTrue(pending) + self.assertEqual(cached_expire, expire) + self.assertTrue(user_dict) + self.assertEqual(user_dict["expire"], expire) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + expire, + ) + + def test_check_update_account_expire_expired_without_auto_update(self): + """Test that expired users stay expired without matching auto update.""" + configuration = self.configuration + expire = 42 + self._update_account_in_db_and_cache(expire) + base_url = "https://ext.example.com" + configuration.migserver_https_mig_oid_url = base_url + configuration.auto_add_oid_user = False + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/page.py" % base_url, + } + + pending, cached_expire, user_dict = check_update_account_expire( + configuration, TEST_USER_DN, environ, min_days_left=14 + ) + + self.assertFalse(pending) + self.assertEqual(cached_expire, expire) + # NOTE: for expired users with cache mark no user_dict is loaded + self.assertEqual(user_dict, None) + self.assertEqual( + get_account_expire_cache(configuration, TEST_USER_DN), + expire, + ) + + def test_check_update_account_expire_missing_user(self): + """Test that missing users are reported as expired.""" + configuration = self.configuration + base_url = "https://oid.example.com" + environ = { + "REMOTE_ADDR": "192.0.2.10", + "HTTP_USER_AGENT": "test agent", + "SCRIPT_URI": "%s/wsgi-bin/something.py" % base_url, + } + with self.assertLogs(level="ERROR") as log_capture: + pending, expire, user_dict = check_update_account_expire( + configuration, OTHER_USER_DN, environ, min_days_left=14 + ) + self.assertFalse(pending) + self.assertEqual(expire, -42) + self.assertIsNone(user_dict) + self.assertTrue( + any("no such account" in msg for msg in log_capture.output) + ) + + if __name__ == "__main__": testmain() From 8ebfce39552f820e5356fd4f72ba457cfe488103 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 23 Jun 2026 14:55:05 +0200 Subject: [PATCH 56/61] Rework handling of UUID user home in check_account_accessible to correctly look up user X509 ID before checking account access. The default X509 user format should remain functionally unchanged despite minor adjustments. Extra care is needed while following the relative alias links from email, short ID and X509 DN to the final UUID user home. Additionally we must be cautious about the link traversal loop always terminating to avoid unpleasant surprises. --- mig/shared/accountstate.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/mig/shared/accountstate.py b/mig/shared/accountstate.py index 8cf30ff51..240ff19cf 100644 --- a/mig/shared/accountstate.py +++ b/mig/shared/accountstate.py @@ -453,17 +453,43 @@ def check_account_accessible(configuration, username, proto, environ=None, # Use client_id_dir to make it work even if already expanded home_dir = os.path.join(configuration.user_home, client_id_dir(client_id)) + # Expand to actual home dir and proceed from there + expanded_home = os.path.realpath(home_dir) if configuration.site_user_id_format == X509_USER_ID_FORMAT: - # Expand to actual home dir - expanded_home = os.path.realpath(home_dir) + client_dir = os.path.basename(expanded_home) elif configuration.site_user_id_format == UUID_USER_ID_FORMAT: - # Only follow first username -> client_id_dir symlink with UUID - expanded_home = os.readlink(home_dir) + # Traverse home_dir links to X509 dir name linking to expanded_home + next_link = home_dir + client_dir = None + expansions, max_expansions = 0, 3 + while os.path.islink(next_link) and expansions < max_expansions: + _logger.debug("compare next link %s to expanded %s" % + (next_link, expanded_home)) + if os.readlink(next_link) == expanded_home: + # Next link is the wanted X509 link to expanded UUID home + _logger.debug("found %s account ID dir: %s -> %s" % + (client_id, next_link, expanded_home)) + client_dir = os.path.basename(next_link) + break + else: + # follow next_link and prepend user_home if relative + next_link = os.readlink(next_link) + max_expansions += 1 + if os.path.basename(next_link) == next_link: + next_link = os.path.join(configuration.user_home, + next_link) + if client_dir is None: + _logger.debug("link traversal for %s ended at %s" % + (client_id, next_link)) + _logger.warning("failed to lookup home and ID for %s" % + username) + return False else: raise ValueError("invalid user ID format requested: %s" % configuration.site_user_id_format) - real_id = os.path.basename(expanded_home) - client_id = client_dir_id(real_id) + + # Now we can extract the desired X509 user ID + client_id = client_dir_id(client_dir) (account_accessible, account_status, _) = check_account_status( configuration, client_id) From 720c455587ed861738a1689a9aa4f9d6e61f55ba Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 23 Jun 2026 15:06:08 +0200 Subject: [PATCH 57/61] Resync with test suite in #580 and extend coverage to some of the UUID user ID format cases. Also tests the recently adjusted check_account_accessible helper thoroughly. --- tests/test_mig_shared_accountstate.py | 638 ++++++++++++++++++++++++-- 1 file changed, 611 insertions(+), 27 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 2fc5254c5..3273b161f 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -27,6 +27,7 @@ """Unit tests for the mig shared accountstate module""" +import base64 import os import time import unittest @@ -49,19 +50,22 @@ ) # Imports required for the unit test wrapping -from mig.shared.base import client_id_dir +from mig.shared.base import client_id_dir, distinguished_name_to_user from mig.shared.defaults import ( AUTH_CERTIFICATE, AUTH_GENERIC, AUTH_OPENID_CONNECT, AUTH_OPENID_V2, + DEFAULT_USER_ID_FORMAT, + UUID_USER_ID_FORMAT, cert_auto_extend_days, expire_marks_dir, + keyword_auto, oid_auto_extend_days, oidc_auto_extend_days, status_marks_dir, ) -from mig.shared.useradm import _ensure_dirs_needed_for_userdb +from mig.shared.useradm import _ensure_dirs_needed_for_userdb, create_user from mig.shared.userdb import load_user_dict, update_user_dict # Imports required for the unit tests themselves @@ -86,9 +90,43 @@ ) TEST_USER_EMAIL = TEST_USER_DN.split("/emailAddress=", 1)[-1] TEST_USER_DIR = client_id_dir(TEST_USER_DN) +# IMPORTANT: TEST_USER_UUID is *generated* as base16 of DN here OTHER_USER_DIR = client_id_dir(OTHER_USER_DN) +def _provision_uuid_test_user(configuration, client_id, client_overrides=None): + """Helper to provision test users when UUID format is used""" + # TODO: merge something like this version into standard _provision_test_user? + # IMPORTANT: we need to use explicit create_user here for UUID format! + user_dict = distinguished_name_to_user(client_id) + # NOTE: generate unique and short id based on id to avoid test collisions + user_dict["unique_id"] = base64.b16encode(client_id.encode("utf8")).decode( + "ascii" + ) + user_dict["short_id"] = base64.b16encode( + user_dict["email"].encode("utf8") + ).decode("ascii") + user_dict["comment"] = "This is the user account comment" + user_dict["locality"] = "" + user_dict["organizational_unit"] = "" + user_dict["password"] = "" + user_dict["password_hash"] = "" + if client_overrides is not None: + user_dict.update(client_overrides) + + create_user( + user_dict, + configuration, + keyword_auto, + default_renew=True, + ask_renew=False, + ) + return user_dict + + +# TODO: test gdp usernames, too + + class TestMigSharedAccountstate__default_account_valid_days(MigTestCase): """Coverage of accountstate default_account_valid_days function.""" @@ -685,23 +723,54 @@ def test_check_account_expire_no_expire_field(self): self.assertTrue(pending) self.assertEqual(expire, -1) + # TODO: handle underlying type errors gracefully and drop this test def test_check_account_expire_invalid_expire_type(self): - """Test that check_account_expire returns expired if expire is not a number.""" + """Test that check_account_expire fails if expire is not a valid type.""" configuration = self.configuration logger = self.logger - user_dict = { - "distinguished_name": TEST_USER_DN, - "expire": "invalid", - } - update_user_dict( - logger, TEST_USER_DN, user_dict, self.expected_user_db_file - ) - with self.assertRaises(TypeError): + + # Invalid values that will trigger TypeError in function under test + for invalid_expire in ( + "invalid", + "-41", + "-41.2", + "111111111141.2", + (1, 2), + ): + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": invalid_expire, + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + # Make sure cache doesn't interfere with parsing + update_account_expire_cache(configuration, user_dict, delete=True) + # Ugly but TypeError here correctly prevents further login use + with self.assertRaises(TypeError): + _ = check_account_expire(configuration, TEST_USER_DN) + + def test_check_account_expire_invalid_expire_value(self): + """Test that check_account_expire says expired if expire is an invalid value.""" + configuration = self.configuration + logger = self.logger + + # Invalid values that will fail without error in function under test + for invalid_expire in ("4242", None, False): + user_dict = { + "distinguished_name": TEST_USER_DN, + "expire": invalid_expire, + } + update_user_dict( + logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + # Make sure cache doesn't interfere with parsing + update_account_expire_cache(configuration, user_dict, delete=True) + # Assure these values return expired to prevent further login use pending, expire, _ = check_account_expire( configuration, TEST_USER_DN ) self.assertFalse(pending) - self.assertEqual(expire, -42) def test_check_account_expire_no_user_db_entry(self): """Test that check_account_expire returns expired if user is not in the DB.""" @@ -982,7 +1051,8 @@ def test_account_expire_info_about_to_expire_oid_no_auto_renew(self): self.assertTrue(expire_warn) self.assertEqual(account_expire, expect_expire) self.assertEqual(renew_days, configuration.oid_valid_days) - self.assertEqual(extend_days, 0) # Because auto-renew is disabled + # NOTE: should be 0 because auto-renew is disabled + self.assertEqual(extend_days, 0) def test_account_expire_info_with_different_min_days_left(self): """Test account_expire_info with a custom min_days_left.""" @@ -1042,9 +1112,9 @@ def _provide_configuration(self): def before_each(self): """Create the directories that the function may touch.""" configuration = self.configuration - self.configuration.site_enable_sharelinks = True - self.configuration.site_enable_jobs = True - self.configuration.site_enable_jupyter = True + configuration.site_enable_sharelinks = True + configuration.site_enable_jobs = True + configuration.site_enable_jupyter = True # Ensure the home directories that the function may reference exist ensure_dirs_exist(configuration.user_home) ensure_dirs_exist(configuration.mrsl_files_dir) @@ -1263,7 +1333,7 @@ def test_check_account_status_missing_user(self): accessible, _, _ = check_account_status( configuration, OTHER_USER_DN ) - self.assertFalse(accessible) + self.assertFalse(accessible) self.assertTrue( any("no such account" in msg for msg in log_capture.output) ) @@ -1280,9 +1350,11 @@ def _provide_configuration(self): def before_each(self): """Set up test environment for check_account_accessible tests.""" configuration = self.configuration - self.configuration.site_enable_sharelinks = True - self.configuration.site_enable_jobs = True - self.configuration.site_enable_jupyter = True + # Force X509 user ID format + configuration.site_user_id_format = DEFAULT_USER_ID_FORMAT + configuration.site_enable_sharelinks = True + configuration.site_enable_jobs = True + configuration.site_enable_jupyter = True # Ensure necessary directories exist ensure_dirs_exist(configuration.mig_system_files) ensure_dirs_exist( @@ -1557,13 +1629,476 @@ def test_check_account_accessible_io_expire_disabled(self): update_account_expire_cache(configuration, user_dict) update_account_status_cache(configuration, user_dict) + # NOTE: should succeed because IO expire is disabled + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp", io_login=True + ) + self.assertTrue(accessible) + + def test_check_account_accessible_openid_expire_disabled_direct(self): + """Test that account remains accessible on OpenID (non-IO) with direct DN when expire is not enforced.""" + configuration = self.configuration + configuration.user_openid_enforce_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because OpenID expire is disabled + # NOTE: we can also check with DN even if not really used + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oid", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_openid_expire_disabled_on_email_alias( + self, + ): + """Test that account remains accessible on OpenID (non-IO) with email when expire is not enforced.""" + configuration = self.configuration + configuration.user_openid_enforce_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because OpenID expire is disabled + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "oid", io_login=False + ) + self.assertTrue(accessible) + + +class TestMigSharedAccountstate__check_account_accessible_uuid_user_id( + MigTestCase, UserAssertMixin +): + """Coverage of accountstate check_account_accessible function with UUID user format.""" + + def _provide_configuration(self): + return "testconfig" + + def before_each(self): + """Set up test environment for check_account_accessible tests.""" + configuration = self.configuration + # Force UUID user ID format + configuration.site_user_id_format = UUID_USER_ID_FORMAT + configuration.site_enable_sharelinks = True + configuration.site_enable_jobs = True + configuration.site_enable_jupyter = True + # Ensure necessary directories exist + ensure_dirs_exist(configuration.mig_system_files) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, expire_marks_dir) + ) + ensure_dirs_exist( + os.path.join(configuration.mig_system_run, status_marks_dir) + ) + # Set up user DB and keep paths for later use + _ensure_dirs_needed_for_userdb(configuration) + self.expected_user_db_home = os.path.normpath( + configuration.user_db_home + ) + self.expected_user_db_file = os.path.join( + self.expected_user_db_home, "MiG-users.db" + ) + # Provision a known test user on UUID format + self.test_user_dict = _provision_uuid_test_user( + configuration, TEST_USER_DN + ) + self.test_user_id = self.test_user_dict["unique_id"] + self.test_user_short_id = self.test_user_dict["short_id"] + + self.test_user_home_path = os.path.join( + configuration.user_home, self.test_user_id + ) + # Verify home dir is functional + self.assertTrue(os.path.isdir(self.test_user_home_path)) + # This simulates an existing short id link to X509 on to uuid dir in user_home + x509_link_in_home = os.path.join( + configuration.user_home, TEST_USER_DIR) + if not os.path.islink(x509_link_in_home): + os.symlink(self.test_user_id, x509_link_in_home) + # Verify link is functional + self.assertTrue(os.path.islink(x509_link_in_home)) + self.assertEqual( + os.path.realpath(x509_link_in_home), self.test_user_home_path + ) + email_link_in_home = os.path.join( + configuration.user_home, TEST_USER_EMAIL + ) + if not os.path.islink(email_link_in_home): + os.symlink(TEST_USER_DIR, email_link_in_home) + # Verify link is functional + self.assertTrue(os.path.islink(email_link_in_home)) + self.assertEqual( + os.path.realpath(email_link_in_home), self.test_user_home_path + ) + short_id_link_in_home = os.path.join( + configuration.user_home, self.test_user_short_id + ) + if not os.path.islink(short_id_link_in_home): + os.symlink(TEST_USER_DIR, short_id_link_in_home) + # Verify link is functional + self.assertTrue(os.path.islink(short_id_link_in_home)) + self.assertEqual( + os.path.realpath(short_id_link_in_home), self.test_user_home_path + ) + + def test_check_account_accessible_active_user_web_access_direct(self): + """Test that an active UUID user is accessible via web (non-IO).""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oidc", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_active_user_io_access_direct(self): + """Test that an active UUID user is accessible via SFTP (IO).""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_active_user_web_access_on_email_alias( + self, + ): + """Test that an active UUID user is accessible via web (non-IO) using email alias.""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "oidc", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_active_user_sftp_access_on_email_alias( + self, + ): + """Test that an active UUID user is accessible via SFTP using email alias.""" + configuration = self.configuration + # Ensure the user is active and not expired + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": time.time() + 42 * 24 * 3600, # far future + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_locked_user_web_blocked(self): + """Test that a locked UUID user is not accessible on web (non-IO).""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": TEST_STATUS_LOCKED, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oidc", io_login=False + ) + self.assertFalse(accessible) + + def test_check_account_accessible_locked_user_io_blocked(self): + """Test that a locked UUID user is not accessible on SFTP (IO).""" + configuration = self.configuration + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": TEST_STATUS_LOCKED, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp" + ) + self.assertFalse(accessible) + + def test_check_account_accessible_expired_user_blocked(self): + """Test that an expired UUID user is inaccessible on web (non-ID) login.""" + configuration = self.configuration + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "oidc", io_login=False + ) + self.assertFalse(accessible) + + def test_check_account_accessible_expired_user_sftp_access_blocked(self): + """Test that an expired UUID user is inaccessible on sftp if enenforced.""" + configuration = self.configuration + configuration.site_io_account_expire = True + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + accessible = check_account_accessible( + configuration, TEST_USER_DN, "sftp" + ) + self.assertFalse(accessible) + + def test_check_account_accessible_special_login_sharelink_access(self): + """Test that a UUID sharelink is detected as special login and accessible.""" + configuration = self.configuration + # Create a dummy RW sharelink to user home + sharelink_path = os.path.join( + configuration.sharelink_home, "read-write", TEST_RW_SHARE_ID + ) + sharelink_target = os.path.join(configuration.user_home, TEST_USER_DIR) + ensure_dirs_exist(sharelink_target) + ensure_dirs_exist(os.path.dirname(sharelink_path)) + os.symlink(sharelink_target, sharelink_path) + + accessible = check_account_accessible( + configuration, TEST_RW_SHARE_ID, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_special_login_job_access(self): + """Test that a UUID job ID is detected as special login and accessible.""" + configuration = self.configuration + configuration.site_enable_jobs = True + # Create a dummy job link to mrsl files entry + job_target_path = os.path.join( + configuration.mrsl_files_dir, TEST_JOB_ID + ) + ensure_dirs_exist(job_target_path) + job_link_path = os.path.join( + configuration.sessid_to_mrsl_link_home, TEST_JOB_ID + ".mRSL" + ) + ensure_dirs_exist(os.path.dirname(job_link_path)) + os.symlink(job_target_path, job_link_path) + + accessible = check_account_accessible( + configuration, TEST_JOB_ID, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_special_login_jupyter_access(self): + """Test that a UUID jupyter mount ID is detected as special login and accessible.""" + configuration = self.configuration + configuration.site_enable_jupyter = True + # Create a dummy jupyter‑mount link to user home + jupyter_link_path = os.path.join( + configuration.sessid_to_jupyter_mount_link_home, + TEST_JUPYTER_SESSION_ID, + ) + jupyter_target = os.path.join(configuration.user_home, TEST_USER_DIR) + ensure_dirs_exist(jupyter_target) + ensure_dirs_exist(os.path.dirname(jupyter_link_path)) + os.symlink(jupyter_target, jupyter_link_path) + + accessible = check_account_accessible( + configuration, TEST_JUPYTER_SESSION_ID, "sftp" + ) + self.assertTrue(accessible) + + def test_check_account_accessible_missing_user_web_rejected(self): + """Test that check_account_accessible rejects a missing UUID user on web (non-IO).""" + configuration = self.configuration + # TODO: adjust backend to handle missing link and drop try/except here + try: + accessible = check_account_accessible( + configuration, OTHER_USER_DN, "oidc", io_login=False + ) + except FileNotFoundError: + accessible = False + + self.assertFalse(accessible) + + def test_check_account_accessible_missing_user_io_rejected(self): + """Test that check_account_accessible rejects a missing UUID user on SFTP (IO).""" + configuration = self.configuration + # TODO: adjust backend to handle missing link and drop try/except here + try: + accessible = check_account_accessible( + configuration, OTHER_USER_DN, "sftp" + ) + except FileNotFoundError: + accessible = False + + self.assertFalse(accessible) + + def test_check_account_accessible_io_expire_disabled_on_email_alias(self): + """Test that UUID account remains accessible on SFTP (IO) with email when expire is not enforced.""" + configuration = self.configuration + configuration.site_io_account_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because IO expire is disabled + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "sftp", io_login=True + ) + self.assertTrue(accessible) + + def test_check_account_accessible_io_expire_disabled_on_short_id(self): + """Test that UUID account remains accessible on SFTP (IO) with short id when expire is not enforced.""" + configuration = self.configuration + configuration.site_io_account_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because IO expire is disabled + accessible = check_account_accessible( + configuration, self.test_user_short_id, "sftp", io_login=True + ) + self.assertTrue(accessible) + + def test_check_account_accessible_io_expire_disabled_direct(self): + """Test that UUID account remains accessible on SFTP (IO) with DN when expire is not enforced.""" + configuration = self.configuration + configuration.site_io_account_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because IO expire is disabled accessible = check_account_accessible( configuration, TEST_USER_DN, "sftp", io_login=True ) - self.assertTrue(accessible) # Because IO expire is disabled + self.assertTrue(accessible) + + def test_check_account_accessible_openid_expire_disabled_on_email_alias( + self, + ): + """Test that UUID account remains accessible on OpenID (non-IO) with email when expire is not enforced.""" + configuration = self.configuration + configuration.user_openid_enforce_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) - def test_check_account_accessible_openid_expire_disabled(self): - """Test that account remains accessible on OpenID (non-IO) when expire is not enforced.""" + # NOTE: should succeed because OpenID expire is disabled + accessible = check_account_accessible( + configuration, TEST_USER_EMAIL, "oid", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_openid_expire_disabled_on_short_id(self): + """Test that UUID account remains accessible on OpenID (non-IO) with short id when expire is not enforced.""" configuration = self.configuration configuration.user_openid_enforce_expire = False # Set expire to past @@ -1579,10 +2114,59 @@ def test_check_account_accessible_openid_expire_disabled(self): update_account_expire_cache(configuration, user_dict) update_account_status_cache(configuration, user_dict) + # NOTE: should succeed because OpenID expire is disabled + # NOTE: short_id can be tested here because we explicitly link it + accessible = check_account_accessible( + configuration, self.test_user_short_id, "oid", io_login=False + ) + self.assertTrue(accessible) + + def test_check_account_accessible_openid_expire_disabled_direct(self): + """Test that UUID account remains accessible on OpenID (non-IO) with DN when expire is not enforced.""" + configuration = self.configuration + configuration.user_openid_enforce_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because OpenID expire is disabled accessible = check_account_accessible( configuration, TEST_USER_DN, "oid", io_login=False ) - self.assertTrue(accessible) # Because OpenID expire is disabled + self.assertTrue(accessible) + + @unittest.skip("TODO: enable login with plain UUID and enable test?") + def test_check_account_accessible_openid_expire_disabled_on_uuid(self): + """Test that UUID account remains accessible on OpenID (non-IO) with UUID when expire is not enforced.""" + configuration = self.configuration + configuration.user_openid_enforce_expire = False + # Set expire to past + expire_ts = 42 + user_dict = { + "distinguished_name": TEST_USER_DN, + "status": "active", + "expire": expire_ts, + } + update_user_dict( + self.logger, TEST_USER_DN, user_dict, self.expected_user_db_file + ) + update_account_expire_cache(configuration, user_dict) + update_account_status_cache(configuration, user_dict) + + # NOTE: should succeed because OpenID expire is disabled + accessible = check_account_accessible( + configuration, self.test_user_id, "oid", io_login=False + ) + self.assertTrue(accessible) class TestMigSharedAccountstate__check_update_account_expire( @@ -1868,9 +2452,9 @@ def test_check_update_account_expire_missing_user(self): pending, expire, user_dict = check_update_account_expire( configuration, OTHER_USER_DN, environ, min_days_left=14 ) - self.assertFalse(pending) - self.assertEqual(expire, -42) - self.assertIsNone(user_dict) + self.assertFalse(pending) + self.assertEqual(expire, -42) + self.assertIsNone(user_dict) self.assertTrue( any("no such account" in msg for msg in log_capture.output) ) From c47547d49ac401b8bdd441721e391aa176e8cd08 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 23 Jun 2026 15:48:31 +0200 Subject: [PATCH 58/61] Remove try/except wrapping of calls were the backend function was meanwhile updated to handle it gracefully. --- tests/test_mig_shared_accountstate.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/test_mig_shared_accountstate.py b/tests/test_mig_shared_accountstate.py index 3273b161f..bf40d9824 100644 --- a/tests/test_mig_shared_accountstate.py +++ b/tests/test_mig_shared_accountstate.py @@ -1980,27 +1980,17 @@ def test_check_account_accessible_special_login_jupyter_access(self): def test_check_account_accessible_missing_user_web_rejected(self): """Test that check_account_accessible rejects a missing UUID user on web (non-IO).""" configuration = self.configuration - # TODO: adjust backend to handle missing link and drop try/except here - try: - accessible = check_account_accessible( - configuration, OTHER_USER_DN, "oidc", io_login=False - ) - except FileNotFoundError: - accessible = False - + accessible = check_account_accessible( + configuration, OTHER_USER_DN, "oidc", io_login=False + ) self.assertFalse(accessible) def test_check_account_accessible_missing_user_io_rejected(self): """Test that check_account_accessible rejects a missing UUID user on SFTP (IO).""" configuration = self.configuration - # TODO: adjust backend to handle missing link and drop try/except here - try: - accessible = check_account_accessible( - configuration, OTHER_USER_DN, "sftp" - ) - except FileNotFoundError: - accessible = False - + accessible = check_account_accessible( + configuration, OTHER_USER_DN, "sftp" + ) self.assertFalse(accessible) def test_check_account_accessible_io_expire_disabled_on_email_alias(self): From 9c86f29a4275d5c50885582fc9c7ddbfa80947d1 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Tue, 23 Jun 2026 15:56:30 +0200 Subject: [PATCH 59/61] Bump copyright year. --- mig/shared/accountstate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mig/shared/accountstate.py b/mig/shared/accountstate.py index 240ff19cf..49ffd9683 100644 --- a/mig/shared/accountstate.py +++ b/mig/shared/accountstate.py @@ -4,7 +4,7 @@ # --- BEGIN_HEADER --- # # accountstate - various user account state helpers -# Copyright (C) 2003-2025 The MiG Project by the Science HPC Center at UCPH +# Copyright (C) 2003-2026 The MiG Project by the Science HPC Center at UCPH # # This file is part of MiG. # From c2fbbebbf81cd8d891726993fce4ea02ffc08209 Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 24 Jun 2026 10:15:03 +0200 Subject: [PATCH 60/61] Expose `user_id_format` in generateconfs but with explicit default of X509 and a comment in conf that UUID format is still only experimental. --- mig/install/MiGserver-template.conf | 2 ++ mig/install/generateconfs.py | 1 + mig/shared/install.py | 3 +++ tests/fixture/confs-stdlocal/MiGserver.conf | 2 ++ 4 files changed, 8 insertions(+) diff --git a/mig/install/MiGserver-template.conf b/mig/install/MiGserver-template.conf index 650a8ed7d..3aaa9a777 100644 --- a/mig/install/MiGserver-template.conf +++ b/mig/install/MiGserver-template.conf @@ -523,6 +523,8 @@ update_interval = __ACCOUNTING_UPDATE_INTERVAL__ # Web site appearance # Whether to use Python 3 for all Python invocations prefer_python3 = __PREFER_PYTHON3__ +# Which user home dir format to use (default is X509; only experimental UUID) +user_id_format = __USER_ID_FORMAT__ # Dynamic entry page to pick user default with fallback to site landing page autolaunch_page = __AUTOLAUNCH_PAGE__ # Entry page if not explictly provided or overriden by user diff --git a/mig/install/generateconfs.py b/mig/install/generateconfs.py index 3f50354e9..68049e42a 100755 --- a/mig/install/generateconfs.py +++ b/mig/install/generateconfs.py @@ -174,6 +174,7 @@ def main(argv, _generate_confs=generate_confs, _print=print): 'listen_clause', 'serveralias_clause', 'distro', + 'user_id_format', 'autolaunch_page', 'landing_page', 'skin', diff --git a/mig/shared/install.py b/mig/shared/install.py index 7184221f0..20f7c381e 100644 --- a/mig/shared/install.py +++ b/mig/shared/install.py @@ -500,6 +500,7 @@ def generate_confs( listen_clause='#Listen', serveralias_clause='#ServerAlias', distro='Debian', + user_id_format='X509', autolaunch_page=None, landing_page=None, skin='migrid-basic', @@ -833,6 +834,7 @@ def _generate_confs_prepare( listen_clause, serveralias_clause, distro, + user_id_format, autolaunch_page, landing_page, skin, @@ -1094,6 +1096,7 @@ def _generate_confs_prepare( user_dict['__LISTEN_CLAUSE__'] = listen_clause user_dict['__SERVERALIAS_CLAUSE__'] = serveralias_clause user_dict['__DISTRO__'] = distro + user_dict['__USER_ID_FORMAT__'] = user_id_format user_dict['__SKIN__'] = skin user_dict['__TITLE__'] = title user_dict['__SHORT_TITLE__'] = short_title diff --git a/tests/fixture/confs-stdlocal/MiGserver.conf b/tests/fixture/confs-stdlocal/MiGserver.conf index 1e5814010..05b1a89a4 100644 --- a/tests/fixture/confs-stdlocal/MiGserver.conf +++ b/tests/fixture/confs-stdlocal/MiGserver.conf @@ -523,6 +523,8 @@ update_interval = 3600 # Web site appearance # Whether to use Python 3 for all Python invocations prefer_python3 = False +# Which user home dir format to use (default is X509; only experimental UUID) +user_id_format = X509 # Dynamic entry page to pick user default with fallback to site landing page autolaunch_page = /wsgi-bin/autolaunch.py # Entry page if not explictly provided or overriden by user From 003334939c183055efbe78841ae735f068d2038a Mon Sep 17 00:00:00 2001 From: Jonas Bardino Date: Wed, 24 Jun 2026 14:37:12 +0200 Subject: [PATCH 61/61] Fix a corner-case bug in the lookup_client_id_from_uuid where it would break if both the UUID -> X509 link and say a leftover matching reverse X509 to UUID link exist in the `user_id_alias_dir`. Includes a regression test initially used to confirm its existence and assure that it is now really fixed. --- mig/shared/useradm.py | 3 ++- tests/test_mig_shared_useradm.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mig/shared/useradm.py b/mig/shared/useradm.py index 324013e9c..c5302f774 100644 --- a/mig/shared/useradm.py +++ b/mig/shared/useradm.py @@ -296,7 +296,8 @@ def lookup_client_id_from_uuid(configuration, user_id): _logger.debug("looking for alias link %r" % alias_link) # NOTE: use islink rather than exists here because it is a dead link if so if os.path.islink(alias_link): - target_path = os.path.realpath(alias_link) + # Only follow first chain even if mix of UUID and X509 links overlap + target_path = os.readlink(alias_link) client_dir = os.path.basename(target_path) _logger.debug("found linked alias %r for %r" % (client_dir, user_id)) else: diff --git a/tests/test_mig_shared_useradm.py b/tests/test_mig_shared_useradm.py index 7822fc914..81a04d9d7 100644 --- a/tests/test_mig_shared_useradm.py +++ b/tests/test_mig_shared_useradm.py @@ -1731,6 +1731,28 @@ def test_lookup_client_id_from_uuid_via_reverse_lookup(self): link_target = os.path.basename(os.path.realpath(alias_link_in_run)) self.assertEqual(link_target, client_dir) + def test_lookup_client_id_from_uuid_via_alias_link_despite_reverse_link(self): + """Test lookup via existing alias link in mig_system_run even when reverse X509 link exists""" + configuration = self.configuration + client_id = TEST_USER_DN + user_id = TEST_USER_UUID + # Setup real directory for the client and alias link in mig_system_run + real_dir = os.path.join(configuration.user_home, user_id) + ensure_dirs_exist(real_dir) + alias_link = os.path.join( + configuration.mig_system_run, user_id_alias_dir, user_id) + ensure_dirs_exist(os.path.dirname(alias_link)) + client_dir = client_id_dir(client_id) + os.symlink(client_dir, alias_link) + # Make the additional reverse link from X509 to UUID in mig_system_run + reverse_link = os.path.join( + configuration.mig_system_run, user_id_alias_dir, client_dir) + os.symlink(user_id, reverse_link) + + # Assure reverse link doesn't interfere + result = lookup_client_id_from_uuid(configuration, user_id) + self.assertEqual(result, client_id) + def test_lookup_client_id_from_uuid_fails_with_only_short_id_link(self): """Test lookup via reverse lookup in user_home fails on short id link""" configuration = self.configuration