{"record":{"id":"cbac398bd726bb17","repo":"langgenius/dify","slug":"builtin-rbac-role-not-found-for-tenant-tenant-id","errorCode":null,"errorMessage":"Builtin RBAC role not found for tenant={tenant_id}, legacy_role={legacy_role}","messagePattern":"Builtin RBAC role not found for tenant=(.+?), legacy_role=(.+?)","errorType":"console","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"api/commands/rbac.py","lineNumber":51,"sourceCode":"    identified by runtime ids, so the command must look them up per tenant.\n    \"\"\"\n    roles = RBACService.Roles.list(\n        tenant_id=tenant_id,\n        account_id=operator_account_id,\n        options=ListOption(page_number=1, results_per_page=100),\n    ).data\n    role_id_by_tag = {\n        role.role_tag: role.id\n        for role in roles\n        if role.is_builtin and role.category == \"global_system_default\" and role.role_tag\n    }\n    resolved: dict[str, str] = {}\n    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(","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/commands/rbac.py#L33-L69","documentation":"ValueError raised in _resolve_builtin_role_ids when one of the expected builtin RBAC role tags (owner, admin, editor, normal, dataset_operator) is not present among the tenant's global_system_default builtin roles returned by RBACService.Roles.list. Each legacy workspace role maps to an expected builtin tag; if that tag's role id is missing, the migration cannot map members.","triggerScenarios":"Triggered when the RBAC roles list for the tenant (page 1, 100 per page) lacks a role whose role_tag equals the expected builtin tag and whose is_builtin is true and category is 'global_system_default'. Also note dataset_operator is skipped when DATASET_OPERATOR_ENABLED is false.","commonSituations":"The tenant's builtin roles were not seeded (incomplete RBAC initialization), the role catalog changed in a version upgrade, pagination missed roles because there are more than 100 builtin roles, or the operator account lacks permission to list roles.","solutions":["Ensure RBAC builtin role seeding ran for this tenant (check the roles API or DB for owner/admin/editor/normal tags).","Verify the operator account used by the command has permission to list roles for the tenant.","If DATASET_OPERATOR_ENABLED is false intentionally, confirm the failing role is not dataset_operator (which is expected to be skipped).","If the tenant has >100 builtin roles, increase the ListOption results_per_page."],"exampleFix":"# before - roles not seeded\nflask rbac-migrate-member-roles  # raises\n\n# after - seed builtin roles first\nflask rbac-seed-builtin-roles && flask rbac-migrate-member-roles","handlingStrategy":"validation","validationCode":"def builtin_roles_present(tenant_id, operator_account_id) -> bool:\n    roles = RBACService.Roles.list(\n        tenant_id=tenant_id, account_id=operator_account_id,\n        options=ListOption(page_number=1, results_per_page=100),\n    ).data\n    tags = {r.role_tag for r in roles if r.is_builtin and r.category == \"global_system_default\"}\n    required = set(_LEGACY_ROLE_TO_BUILTIN_TAG.values())\n    if not dify_config.DATASET_OPERATOR_ENABLED:\n        required.discard(\"dataset_operator\")\n    return required.issubset(tags)","typeGuard":"def has_required_builtin_tags(role_tags: set[str]) -> bool:\n    required = set(_LEGACY_ROLE_TO_BUILTIN_TAG.values())\n    if not dify_config.DATASET_OPERATOR_ENABLED:\n        required.discard(\"dataset_operator\")\n    return required.issubset(role_tags)","tryCatchPattern":"try:\n    role_ids = _resolve_builtin_role_ids(tenant_id, operator_account_id)\nexcept ValueError as exc:\n    click.echo(f\"RBAC role catalog incomplete: {exc}\", err=True)\n    raise click.Abort()","preventionTips":["Run builtin role seeding before member-role migration.","Verify the operator account can list builtin roles for each tenant.","If a tenant has >100 builtin roles, increase results_per_page.","Toggle DATASET_OPERATOR_ENABLED only when consistent with legacy data."],"tags":["backend","cli","rbac","migration","role-resolution","missing-data"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}