fix: harden profiles migration helper
This commit is contained in:
+144
-42
@@ -25,34 +25,43 @@ class TestMigrateProfiles(unittest.TestCase):
|
||||
"version": 1,
|
||||
"profiles": {
|
||||
"prgs": {
|
||||
"base_url": "https://gitea.prgs.cc",
|
||||
"username": "jcwalker3",
|
||||
"auth": {
|
||||
"type": "keychain",
|
||||
"id": "prgs-gitea-token"
|
||||
},
|
||||
"default_owner": "Scaled-Tech-Consulting",
|
||||
"execution_profile": "personal-prgs"
|
||||
"base_url": "redacted-prgs-service",
|
||||
"username": "jcwalker3",
|
||||
"auth": {
|
||||
"type": "keychain",
|
||||
"id": "redacted-author-ref"
|
||||
},
|
||||
"default_owner": "Scaled-Tech-Consulting",
|
||||
"execution_profile": "personal-prgs",
|
||||
"allowed_operations": ["read", "comment"],
|
||||
"forbidden_operations": ["approve", "merge"]
|
||||
},
|
||||
"mdcps": {
|
||||
"base_url": "https://gitea.dadeschools.net",
|
||||
"username": "913443",
|
||||
"auth": {
|
||||
"type": "keychain",
|
||||
"id": "mdcps-gitea-token"
|
||||
},
|
||||
"default_owner": "Contractor",
|
||||
"execution_profile": "mdcps"
|
||||
"base_url": "redacted-mdcps-service",
|
||||
"username": "913443",
|
||||
"auth": {
|
||||
"type": "keychain",
|
||||
"id": "redacted-mdcps-ref"
|
||||
},
|
||||
"default_owner": "Contractor",
|
||||
"execution_profile": "mdcps",
|
||||
"allowed_operations": ["read"],
|
||||
"forbidden_operations": ["merge"]
|
||||
},
|
||||
"prgs-reviewer": {
|
||||
"base_url": "https://gitea.prgs.cc",
|
||||
"username": "sysadmin",
|
||||
"auth": {
|
||||
"type": "keychain",
|
||||
"id": "prgs-gitea-reviewer-token"
|
||||
},
|
||||
"default_owner": "Scaled-Tech-Consulting",
|
||||
"execution_profile": "prgs-reviewer"
|
||||
"base_url": "redacted-prgs-service",
|
||||
"username": "sysadmin",
|
||||
"auth": {
|
||||
"type": "keychain",
|
||||
"id": "redacted-reviewer-ref"
|
||||
},
|
||||
"default_owner": "Scaled-Tech-Consulting",
|
||||
"execution_profile": "prgs-reviewer",
|
||||
"allowed_operations": [
|
||||
"read", "review", "comment", "approve",
|
||||
"request_changes", "merge"
|
||||
],
|
||||
"forbidden_operations": ["branch", "commit", "push", "open_pr"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,34 +76,34 @@ class TestMigrateProfiles(unittest.TestCase):
|
||||
"""Test the structural transformation and capability mapping."""
|
||||
v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content)
|
||||
self.assertEqual(v2_data["version"], 2)
|
||||
|
||||
|
||||
# Check environment structure
|
||||
envs = v2_data["environments"]
|
||||
self.assertIn("prgs", envs)
|
||||
self.assertIn("mdcps", envs)
|
||||
|
||||
|
||||
# Check service and identity structure
|
||||
prgs_gitea = envs["prgs"]["services"]["gitea"]
|
||||
self.assertEqual(prgs_gitea["base_url"], "https://gitea.prgs.cc")
|
||||
self.assertEqual(prgs_gitea["base_url"], "redacted-prgs-service")
|
||||
self.assertEqual(prgs_gitea["default_owner"], "Scaled-Tech-Consulting")
|
||||
|
||||
|
||||
author = prgs_gitea["identities"]["author"]
|
||||
self.assertEqual(author["role"], "author")
|
||||
self.assertEqual(author["username"], "jcwalker3")
|
||||
self.assertEqual(author["auth"]["id"], "prgs-gitea-token")
|
||||
self.assertIn("push", author["allowed_operations"])
|
||||
|
||||
self.assertEqual(author["auth"]["id"], "redacted-author-ref")
|
||||
self.assertEqual(author["allowed_operations"], ["read", "comment"])
|
||||
self.assertEqual(author["forbidden_operations"], ["approve", "merge"])
|
||||
|
||||
reviewer = prgs_gitea["identities"]["reviewer"]
|
||||
self.assertEqual(reviewer["role"], "reviewer")
|
||||
self.assertEqual(reviewer["username"], "sysadmin")
|
||||
self.assertEqual(reviewer["auth"]["id"], "prgs-gitea-reviewer-token")
|
||||
self.assertEqual(reviewer["auth"]["id"], "redacted-reviewer-ref")
|
||||
self.assertIn("merge", reviewer["allowed_operations"])
|
||||
|
||||
def test_alias_generation(self):
|
||||
"""Test that aliases are correctly generated to support old profile names."""
|
||||
v2_data = migrate_profiles.migrate_v1_to_v2(self.v1_content)
|
||||
aliases = v2_data["aliases"]
|
||||
|
||||
|
||||
self.assertEqual(aliases["prgs"], "prgs.gitea.author")
|
||||
self.assertEqual(aliases["prgs-author"], "prgs.gitea.author")
|
||||
self.assertEqual(aliases["prgs-reviewer"], "prgs.gitea.reviewer")
|
||||
@@ -131,20 +140,113 @@ class TestMigrateProfiles(unittest.TestCase):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
migrate_profiles.main()
|
||||
self.assertEqual(cm.exception.code, 0)
|
||||
|
||||
|
||||
self.assertFalse(os.path.exists(output_file))
|
||||
self.assertFalse(os.path.exists(f"{self.input_file}.bak"))
|
||||
|
||||
|
||||
stdout_output = mock_stdout.getvalue()
|
||||
self.assertIn("DRY-RUN MODE", stdout_output)
|
||||
self.assertIn("version", stdout_output)
|
||||
self.assertIn("environments", stdout_output)
|
||||
self.assertIn("identities", stdout_output)
|
||||
self.assertIn("aliases", stdout_output)
|
||||
self.assertNotIn("redacted-prgs-service", stdout_output)
|
||||
self.assertNotIn("redacted-mdcps-service", stdout_output)
|
||||
self.assertNotIn("redacted-author-ref", stdout_output)
|
||||
self.assertNotIn("redacted-mdcps-ref", stdout_output)
|
||||
self.assertNotIn("redacted-reviewer-ref", stdout_output)
|
||||
self.assertNotIn("keychain", stdout_output)
|
||||
self.assertNotIn("auth", stdout_output)
|
||||
|
||||
def test_dry_run_hides_token_like_values(self):
|
||||
"""Verify dry-run summary does not expose token-like auth metadata."""
|
||||
sensitive = json.loads(json.dumps(self.v1_content))
|
||||
sensitive["profiles"]["prgs"]["auth"]["id"] = "super-secret-token-value"
|
||||
with open(self.input_file, "w") as f:
|
||||
json.dump(sensitive, f)
|
||||
|
||||
test_args = ["migrate_profiles.py", "-i", self.input_file]
|
||||
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
|
||||
with patch.object(sys, "argv", test_args):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
migrate_profiles.main()
|
||||
self.assertEqual(cm.exception.code, 0)
|
||||
|
||||
stdout_output = mock_stdout.getvalue()
|
||||
self.assertNotIn("super-secret-token-value", stdout_output)
|
||||
self.assertNotIn("token", stdout_output.lower())
|
||||
|
||||
def test_explicit_operations_are_preserved(self):
|
||||
"""Explicit v1 permissions must not be replaced by role defaults."""
|
||||
v1_data = json.loads(json.dumps(self.v1_content))
|
||||
v1_data["profiles"]["prgs-reviewer"]["allowed_operations"] = ["read"]
|
||||
v1_data["profiles"]["prgs-reviewer"]["forbidden_operations"] = ["merge"]
|
||||
|
||||
v2_data = migrate_profiles.migrate_v1_to_v2(v1_data)
|
||||
reviewer = (
|
||||
v2_data["environments"]["prgs"]["services"]["gitea"]
|
||||
["identities"]["reviewer"]
|
||||
)
|
||||
self.assertEqual(reviewer["allowed_operations"], ["read"])
|
||||
self.assertEqual(reviewer["forbidden_operations"], ["merge"])
|
||||
|
||||
def test_inferred_role_defaults_only_when_unambiguous(self):
|
||||
"""Role defaults are allowed only for clear author/reviewer profiles."""
|
||||
v1_data = {
|
||||
"version": 1,
|
||||
"profiles": {
|
||||
"prgs-author": {
|
||||
"base_url": "redacted-prgs-service",
|
||||
"username": "jcwalker3",
|
||||
"auth": {"type": "keychain", "id": "hidden-author-ref"},
|
||||
"execution_profile": "prgs-author",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
v2_data = migrate_profiles.migrate_v1_to_v2(v1_data)
|
||||
author = (
|
||||
v2_data["environments"]["prgs"]["services"]["gitea"]
|
||||
["identities"]["author"]
|
||||
)
|
||||
self.assertEqual(
|
||||
author["allowed_operations"],
|
||||
migrate_profiles.AUTHOR_DEFAULT_ALLOWED,
|
||||
)
|
||||
self.assertEqual(
|
||||
author["forbidden_operations"],
|
||||
migrate_profiles.AUTHOR_DEFAULT_FORBIDDEN,
|
||||
)
|
||||
|
||||
def test_ambiguous_permission_source_fails_closed(self):
|
||||
"""A profile without explicit permissions or clear role must not widen."""
|
||||
v1_data = {
|
||||
"version": 1,
|
||||
"profiles": {
|
||||
"prgs": {
|
||||
"base_url": "redacted-prgs-service",
|
||||
"username": "jcwalker3",
|
||||
"auth": {"type": "keychain", "id": "hidden-author-ref"},
|
||||
"execution_profile": "personal-prgs",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "fail closed"):
|
||||
migrate_profiles.migrate_v1_to_v2(v1_data)
|
||||
|
||||
def test_partial_permission_source_fails_closed(self):
|
||||
"""Allowed without forbidden, or vice versa, is ambiguous."""
|
||||
v1_data = json.loads(json.dumps(self.v1_content))
|
||||
del v1_data["profiles"]["prgs"]["forbidden_operations"]
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "fail closed"):
|
||||
migrate_profiles.migrate_v1_to_v2(v1_data)
|
||||
|
||||
def test_write_mode_and_backup(self):
|
||||
"""Verify that write mode creates a backup and correctly saves the validated config."""
|
||||
output_file = os.path.join(self.temp_dir, "migrated.json")
|
||||
backup_file = os.path.join(self.temp_dir, "profiles_backup.json.bak")
|
||||
|
||||
|
||||
test_args = [
|
||||
"migrate_profiles.py",
|
||||
"-i", self.input_file,
|
||||
@@ -154,14 +256,14 @@ class TestMigrateProfiles(unittest.TestCase):
|
||||
]
|
||||
with patch.object(sys, "argv", test_args):
|
||||
migrate_profiles.main()
|
||||
|
||||
|
||||
# Verify backup exists and matches original v1 config
|
||||
self.assertTrue(os.path.exists(backup_file))
|
||||
with open(backup_file, "r") as f:
|
||||
backup_data = json.load(f)
|
||||
self.assertEqual(backup_data["version"], 1)
|
||||
self.assertIn("prgs", backup_data["profiles"])
|
||||
|
||||
|
||||
# Verify migrated v2 config exists and validates
|
||||
self.assertTrue(os.path.exists(output_file))
|
||||
with open(output_file, "r") as f:
|
||||
@@ -175,7 +277,7 @@ class TestMigrateProfiles(unittest.TestCase):
|
||||
bad_json_file = os.path.join(self.temp_dir, "bad.json")
|
||||
with open(bad_json_file, "w") as f:
|
||||
f.write("{invalid-json}")
|
||||
|
||||
|
||||
test_args = ["migrate_profiles.py", "-i", bad_json_file]
|
||||
with patch.object(sys, "argv", test_args):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
@@ -185,7 +287,7 @@ class TestMigrateProfiles(unittest.TestCase):
|
||||
bad_version_file = os.path.join(self.temp_dir, "bad_version.json")
|
||||
with open(bad_version_file, "w") as f:
|
||||
json.dump({"version": 3, "profiles": {}}, f)
|
||||
|
||||
|
||||
test_args = ["migrate_profiles.py", "-i", bad_version_file]
|
||||
with patch.object(sys, "argv", test_args):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
|
||||
Reference in New Issue
Block a user