{"record":{"id":"d800f1869f457609","repo":"langgenius/dify","slug":"unsupported-legacy-workspace-role-legacy-role","errorCode":null,"errorMessage":"Unsupported legacy workspace role: {legacy_role}","messagePattern":"Unsupported legacy workspace role: (.+?)","errorType":"console","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"api/commands/rbac.py","lineNumber":64,"sourceCode":"    for legacy_role, expected_builtin_tag in _LEGACY_ROLE_TO_BUILTIN_TAG.items():\n        role_id = role_id_by_tag.get(expected_builtin_tag)\n        if expected_builtin_tag == \"dataset_operator\" and not dify_config.DATASET_OPERATOR_ENABLED:\n            continue\n        if not role_id:\n            raise ValueError(f\"Builtin RBAC role not found for tenant={tenant_id}, legacy_role={legacy_role}\")\n        resolved[legacy_role] = role_id\n    return resolved\n\n\ndef _resolve_builtin_role_id(tenant_id: str, operator_account_id: str, legacy_role: str) -> str:\n    \"\"\"Resolve a legacy workspace role to the current tenant's builtin RBAC role id.\n\n    The migration replays the old `TenantAccountJoin.role` values onto the\n    RBAC member-role binding API. Builtin RBAC roles are tenant-scoped and\n    identified by runtime ids, so the command must look them up per tenant.\n    \"\"\"\n    if legacy_role not in _LEGACY_ROLE_TO_BUILTIN_TAG:\n        raise ValueError(f\"Unsupported legacy workspace role: {legacy_role}\")\n\n    return _resolve_builtin_role_ids(tenant_id, operator_account_id)[legacy_role]\n\n\ndef _iter_tenant_member_batches(\n    tenant_id: str | None,\n    *,\n    db_batch_size: int,\n    api_batch_size: int,\n) -> Iterator[tuple[str, str, list[tuple[str, str]]]]:\n    \"\"\"Yield legacy member roles in tenant-scoped API-sized batches.\n\n    Rows are projected to primitive values and streamed from the database, so\n    the command never materializes every TenantAccountJoin ORM object. The\n    iterator only keeps one tenant's API-sized batches in memory while it\n    finds that tenant's owner account.\n    \"\"\"\n    with session_factory.create_session() as session:","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/commands/rbac.py#L46-L82","documentation":"ValueError raised by _resolve_builtin_role_id when the supplied legacy_role string is not a key in _LEGACY_ROLE_TO_BUILTIN_TAG (owner/admin/editor/normal/dataset_operator). This guards against unknown role values before attempting tenant-scoped lookup.","triggerScenarios":"Triggered when _resolve_builtin_role_id is called with a legacy_role value not present in the TenantAccountRole enum values mapped in _LEGACY_ROLE_TO_BUILTIN_TAG.","commonSituations":"A new role was added to TenantAccountRole but not to _LEGACY_ROLE_TO_BUILTIN_TAG, or a corrupt/custom role string exists in TenantAccountJoin.role in the database.","solutions":["Inspect the actual legacy_role value being passed; query TenantAccountJoin.role distinct values for the tenant.","If the role is legitimate, add its mapping to _LEGACY_ROLE_TO_BUILTIN_TAG with the correct builtin tag.","If the role is corrupt data, clean the TenantAccountJoin rows or filter them out before migration.","Add a regression test covering any new TenantAccountRole enum member."],"exampleFix":"# before\n_LEGACY_ROLE_TO_BUILTIN_TAG = {\n    TenantAccountRole.OWNER.value: \"owner\",\n    TenantAccountRole.ADMIN.value: \"admin\",\n}\n# legacy_role=\"dataset_operator\" -> raises\n\n# after\n_LEGACY_ROLE_TO_BUILTIN_TAG = {\n    TenantAccountRole.OWNER.value: \"owner\",\n    TenantAccountRole.ADMIN.value: \"admin\",\n    TenantAccountRole.DATASET_OPERATOR.value: \"dataset_operator\",\n}","handlingStrategy":"type-guard","validationCode":"def is_supported_legacy_role(role: str) -> bool:\n    return role in _LEGACY_ROLE_TO_BUILTIN_TAG","typeGuard":"def is_supported_legacy_role(role: str) -> bool:\n    return role in _LEGACY_ROLE_TO_BUILTIN_TAG","tryCatchPattern":"try:\n    role_id = _resolve_builtin_role_id(tenant_id, operator_account_id, legacy_role)\nexcept ValueError as exc:\n    click.echo(f\"Skipping unsupported role: {exc}\", err=True)\n    return","preventionTips":["Keep _LEGACY_ROLE_TO_BUILTIN_TAG updated whenever TenantAccountRole gains a member.","Add a unit test that asserts every TenantAccountRole member maps.","Audit distinct TenantAccountJoin.role values before running the migration.","Reject unknown role strings at ingest, not at migration time."],"tags":["backend","cli","rbac","migration","role-resolution","enum-mapping"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}