{"record":{"id":"6d6d759f4e6e942f","repo":"langgenius/dify","slug":"workspace-owner-not-found-for-tenant-current-tena","errorCode":null,"errorMessage":"Workspace owner not found for tenant={current_tenant_id}","messagePattern":"Workspace owner not found for tenant=(.+?)","errorType":"console","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"api/commands/rbac.py","lineNumber":102,"sourceCode":"            select(TenantAccountJoin.tenant_id, TenantAccountJoin.account_id, TenantAccountJoin.role)\n            .order_by(TenantAccountJoin.tenant_id.asc(), TenantAccountJoin.id.asc())\n            .execution_options(yield_per=db_batch_size)\n        )\n        if tenant_id:\n            stmt = stmt.where(TenantAccountJoin.tenant_id == tenant_id)\n\n        current_tenant_id: str | None = None\n        owner_account_id: str | None = None\n        batches: list[list[tuple[str, str]]] = []\n        batch: list[tuple[str, str]] = []\n\n        def flush_current_tenant() -> Iterator[tuple[str, str, list[tuple[str, str]]]]:\n            if current_tenant_id is None:\n                return\n            if batch:\n                batches.append(batch.copy())\n            if not owner_account_id:\n                raise ValueError(f\"Workspace owner not found for tenant={current_tenant_id}\")\n            for item in batches:\n                yield current_tenant_id, owner_account_id, item\n\n        for row in session.execute(stmt):\n            workspace_id = str(row.tenant_id)\n            if current_tenant_id is not None and workspace_id != current_tenant_id:\n                yield from flush_current_tenant()\n                owner_account_id = None\n                batches = []\n                batch = []\n            current_tenant_id = workspace_id\n            account_id = str(row.account_id)\n            role = str(row.role)\n            if role == TenantAccountRole.OWNER.value:\n                owner_account_id = account_id\n            batch.append((account_id, role))\n            if len(batch) >= api_batch_size:\n                batches.append(batch)","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/api/commands/rbac.py#L84-L120","documentation":"ValueError raised inside _iter_tenant_member_batches' flush_current_tenant closure when a tenant's accumulated rows contain no member whose role equals TenantAccountRole.OWNER.value. Every workspace is expected to have exactly one owner; without one, the migration cannot determine the operator account that must authorize member-role replacements.","triggerScenarios":"Triggered when, for a given tenant_id, none of the streamed TenantAccountJoin rows have role == 'owner'. This is evaluated at tenant-boundary flush time.","commonSituations":"The owner row was deleted from TenantAccountJoin (orphaned workspace), the owner's role string was changed to a non-owner value via a bad migration, or data corruption removed the ownership binding.","solutions":["Query `SELECT * FROM tenant_account_joins WHERE tenant_id='<id>' AND role='owner';` to confirm the owner row is missing.","Restore or designate an owner for the tenant before re-running the migration.","If the workspace is defunct, exclude its tenant_id via the command's --tenant-id filter.","Audit for role string drift (e.g. 'Owner' vs 'owner') caused by case-sensitive comparisons."],"exampleFix":"-- before\n-- SELECT role, count(*) FROM tenant_account_joins WHERE tenant_id='t1' GROUP BY role;\n--  admin   | 2\n--  normal  | 5\n\n-- after - restore owner\nUPDATE tenant_account_joins SET role='owner' WHERE tenant_id='t1' AND account_id='<owner-acct>';","handlingStrategy":"validation","validationCode":"def tenant_has_owner(session, tenant_id: str) -> bool:\n    from sqlalchemy import select, func\n    return bool(session.scalar(\n        select(func.count()).select_from(TenantAccountJoin)\n        .where(TenantAccountJoin.tenant_id == tenant_id,\n               TenantAccountJoin.role == TenantAccountRole.OWNER.value)\n    ))","typeGuard":"def tenant_has_owner(session, tenant_id: str) -> bool:\n    return session.scalars(\n        select(TenantAccountJoin).where(\n            TenantAccountJoin.tenant_id == tenant_id,\n            TenantAccountJoin.role == TenantAccountRole.OWNER.value,\n        ).limit(1)\n    ).first() is not None","tryCatchPattern":"try:\n    for batch in _iter_tenant_member_batches(...):\n        process(batch)\nexcept ValueError as exc:\n    if \"Workspace owner not found\" in str(exc):\n        click.echo(f\"Orphaned workspace: {exc}. Restoring owner or excluding tenant.\", err=True)\n    raise","preventionTips":["Preflight each tenant for an owner row before running the migration.","Never delete TenantAccountJoin owner rows without transferring ownership.","Use --tenant-id to skip orphaned workspaces.","Audit role strings for case drift ('Owner' vs 'owner')."],"tags":["backend","cli","rbac","migration","data-integrity","missing-owner"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}