import unittest from weknora_access_etl import (ACCESS_TABLES, ADMIN_TARGET_ID, digest, make_plan, migration_sql, role_permissions, stable, TARGET_DATABASE, TARGET_TENANT) from apply_weknora_access import rollback_sql class AccessMigrationTests(unittest.TestCase): def test_editor_covers_grouped_operations_only(self): permissions, _ = role_permissions({"permissions": [ "read", "feature:tasks", "tasks:read", "tasks:write", "tasks:review", "tasks:deliver", "feature:agents", "agents:read", "agents:write", "memories:read", "memories:write", "memories:review", "feature:flows", "flows:read", "feature:knowledge", "kb:read", "kb:write", "feature:digital-humans", "digital_humans:read", "logs:read", "usage:read"]}) expected = [module + "." + action for module in ("content", "flows", "agents", "knowledge", "digital-humans") for action in ("read", "write")] self.assertEqual(permissions, sorted(expected + ["content.review", "content.deliver"])) def test_generic_read_does_not_grant_any_module(self): self.assertEqual(role_permissions({"permissions": ["read"]})[0], []) def test_legacy_reviewer_cannot_gain_export_or_agent_management(self): permissions, notes = role_permissions({"permissions": ["audit:read", "read", "review"]}) self.assertEqual(permissions, ["content.read", "content.review"]) self.assertNotIn("content_read_withheld_because_native_read_also_exposes_export", notes) def test_content_delivery_does_not_grant_generation(self): self.assertEqual(role_permissions({"permissions": ["tasks:review", "tasks:deliver"]})[0], ["content.deliver", "content.read", "content.review"]) self.assertEqual(role_permissions({"permissions": ["tasks:review", "tasks:write"]})[0], ["content.read", "content.review", "content.write"]) self.assertEqual(role_permissions({"permissions": ["tasks:read", "tasks:deliver"]})[0], []) def test_agent_write_does_not_infer_memory_review(self): permissions, _ = role_permissions({"permissions": ["agents:read", "agents:write", "memories:write"]}) self.assertIn("agents.read", permissions) self.assertNotIn("agents.write", permissions) def test_memory_review_does_not_infer_agent_write(self): self.assertEqual(role_permissions({"permissions": ["feature:memories", "memories:review"]})[0], []) def test_old_config_management_does_not_grant_all_settings_or_access(self): permissions, _ = role_permissions({"permissions": ["config:write", "admin:manage", "users:manage"]}) self.assertNotIn("settings.write", permissions) self.assertNotIn("access.write", permissions) def test_source_admin_does_not_create_global_admin(self): self.assertEqual(role_permissions({"permissions": ["*"]})[0], []) def test_stable_role_identity(self): self.assertEqual(stable("editor"), stable("editor")) self.assertNotEqual(stable("editor"), stable("viewer")) def plan(self): value = {"target_database": TARGET_DATABASE, "tenant_id": TARGET_TENANT, "grants": [], "roles": [], "assignments": [], "publications": [], "source_role_definitions": []} value["plan_sha256"] = digest(value) return value def test_sql_defaults_to_rollback_and_does_not_update_or_delete_native_rows(self): sql = migration_sql(self.plan()) self.assertTrue(sql.rstrip().endswith("ROLLBACK;")) self.assertNotIn("UPDATE users", sql) self.assertNotIn("UPDATE tenant_members", sql) self.assertNotIn("DELETE FROM", sql) self.assertIn("m.status='suspended'", sql) self.assertIn("Native resource authorization already exists", sql) self.assertIn("change->'legacy_source_spec'=saved", sql) self.assertIn("change->'mapped_permissions'=spec->'permissions'", sql) def test_tampered_or_wrong_target_plan_is_rejected(self): plan = self.plan() plan["tenant_id"] = 2 with self.assertRaises(ValueError): migration_sql(plan) plan = self.plan() plan["roles"] = [{"permissions": ["access.write"]}] with self.assertRaises(ValueError): migration_sql(plan) def test_explicit_grants_are_not_inferred(self): plan = self.plan() plan["grants"] = [{"kind": "agents"}] plan["plan_sha256"] = digest({k: v for k, v in plan.items() if k != "plan_sha256"}) with self.assertRaises(ValueError): migration_sql(plan) def source_and_target(self): role = {"role": "viewer", "organization_id": 1, "label": "Viewer", "permissions": ["read"]} source = {"roles": [role], "users": [{"id": 1, "enabled": 1}, {"id": 3, "enabled": 1, "organization_id": 1, "role": "viewer"}], "grants": {}, "memory_count": 0, "resources": {key: [] for key in ("agent", "agentskill", "canvas", "knowledgebase", "digitalhuman")}} identity = {"source_user_id": 3, "target_user_id": "user3", "user_tenant": TARGET_TENANT, "is_active": False, "is_system_admin": False, "can_access_all_tenants": False, "user_deleted": None, "source_snapshot": {"role": "viewer"}, "memberships": [ {"tenant_id": TARGET_TENANT, "role": "contributor", "status": "suspended", "deleted_at": None}]} target = {"database": TARGET_DATABASE, "organization": {"source_role_definitions": [role]}, "identities": [{"source_user_id": 1, "target_user_id": ADMIN_TARGET_ID}, identity], "resources": [], "asset_mappings": [], "flow_mappings": [], "access_tables": list(ACCESS_TABLES)} return source, target def build(self, source, target): return make_plan(source, target, {"tenant_id": TARGET_TENANT}, "snapshot") def test_explicit_deny_all_assignment_is_created_for_viewer(self): plan = self.build(*self.source_and_target()) self.assertEqual(len(plan["assignments"]), 1) self.assertEqual(plan["roles"][0]["permissions"], []) self.assertEqual(plan["assignments"][0]["role_id"], stable("viewer")) def test_active_migrated_account_is_rejected(self): source, target = self.source_and_target() target["identities"][1]["is_active"] = True with self.assertRaisesRegex(ValueError, "inactive and suspended"): self.build(source, target) def test_cross_tenant_membership_is_rejected(self): source, target = self.source_and_target() target["identities"][1]["memberships"].append( {"tenant_id": 9, "role": "viewer", "status": "active", "deleted_at": None}) with self.assertRaisesRegex(ValueError, "inactive and suspended"): self.build(source, target) def test_missing_enabled_identity_is_rejected(self): source, target = self.source_and_target() target["identities"].pop() with self.assertRaisesRegex(ValueError, "lacks a durable identity"): self.build(source, target) def test_source_permission_drift_is_rejected(self): source, target = self.source_and_target() target["organization"]["source_role_definitions"] = [] with self.assertRaisesRegex(ValueError, "role definitions differ"): self.build(source, target) def test_rollback_is_scoped_guarded_and_rehearsal_by_default(self): from weknora_access_etl import LEDGER state = {table: [] for table in (*ACCESS_TABLES, LEDGER)} sql = rollback_sql(self.plan(), state, state) self.assertTrue(sql.rstrip().endswith("ROLLBACK;")) self.assertIn("Native edits or missing rows", sql) self.assertIn("New references to migrated access rows", sql) self.assertIn("Account activation changed", sql) self.assertNotIn("DELETE FROM users", sql) self.assertNotIn("DELETE FROM tenant_members", sql) if __name__ == "__main__": unittest.main()