Files
xinmi/scripts/test_weknora_access_etl.py
T
yawan f7fc992f57
Anydoc / anydoc parser engine (push) Canceled after 0s
App / Format, vet, test, and build (push) Canceled after 0s
CLI / test (macos-latest / go 1.26) (push) Canceled after 0s
CLI / test (ubuntu-latest / go 1.26) (push) Canceled after 0s
CLI / test (windows-latest / go 1.26) (push) Canceled after 0s
Build and Push Docker Image / build-ui (push) Canceled after 0s
Build and Push Docker Image / build-docreader (push) Canceled after 0s
Build and Push Docker Image / build-sandbox (push) Canceled after 0s
Build and Push Docker Image / build-app (amd64, linux/amd64, ubuntu-latest) (push) Canceled after 0s
Build and Push Docker Image / build-app (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
DocReader / Python tests and Go client tests (push) Canceled after 0s
Build & Publish (dsh-weknora) / Typecheck, test, pack (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (pinned) (push) Canceled after 0s
Build & Publish (dsh-weknora) / End-to-end inside dsh (latest, advisory) (push) Canceled after 0s
Frontend / Test, type-check, and build (push) Canceled after 0s
Go Lint Cache / warm lint cache (.) (push) Canceled after 0s
Go Lint Cache / warm lint cache (cli) (push) Canceled after 0s
Go Lint Cache / warm lint cache (client) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.10) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.11) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.12) (push) Canceled after 0s
Build & Publish (mcp-server) / Test (Python 3.13) (push) Canceled after 0s
Build and Push Docker Image / merge (push) Canceled after 0s
Build & Publish (dsh-weknora) / Publish to npm (push) Canceled after 0s
Build & Publish (mcp-server) / Build sdist + wheel (push) Canceled after 0s
Build & Publish (mcp-server) / Publish to PyPI (push) Canceled after 0s
WeKnora deployment source with the reviewed wiki identity, summary and recovery fixes
2026-09-22 16:19:06 +08:00

151 lines
7.9 KiB
Python

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()