{"record":{"id":"402aae1f35ecc21f","repo":"apache/superset","slug":"dashboard-was-deleted-and-re-import-requires-can-w","errorCode":null,"errorMessage":"Dashboard was deleted and re-import requires can_write permission to restore it","messagePattern":"Dashboard was deleted and re-import requires can_write permission to restore it","errorType":"exception","errorClass":"ImportFailedError","httpStatus":500,"severity":"error","filePath":"superset/commands/dashboard/importers/v1/utils.py","lineNumber":337,"sourceCode":"        \"Dashboard\",\n    )\n    # `user` is None for background / example-loader paths (no Flask request\n    # user). Combined with ``can_write=True`` (typically from\n    # ``ignore_permissions=True``), the editorship checks in the restore /\n    # overwrite branches below are intentionally skipped because the caller has\n    # already established trust at the command level.\n    user = get_user()\n    if existing := find_existing_for_import(Dashboard, config[\"uuid\"]):\n        if existing.deleted_at is not None:\n            # RESTORE path — re-importing a soft-deleted UUID is an implicit\n            # restore-with-update, a distinct operation from overwriting an\n            # alive row, so it is handled in its own branch.\n            if not can_write:\n                # Case B: don't silently return a soft-deleted row to a caller\n                # without write permission — that would let a dependent import\n                # (e.g. a dashboard zip referencing this dashboard) reattach to\n                # a deleted dashboard.\n                raise ImportFailedError(\n                    \"Dashboard was deleted and re-import requires can_write \"\n                    \"permission to restore it\"\n                )\n            # ``user`` is None on background / example-loader paths (no Flask\n            # request user); combined with ``can_write`` (typically from\n            # ``ignore_permissions=True``) the editorship check is intentionally\n            # skipped because the caller already established trust.\n            if user and (\n                not security_manager.can_access_dashboard(existing)\n                or (\n                    not security_manager.is_editor(existing)\n                    and not security_manager.is_admin()\n                )\n            ):\n                raise ImportFailedError(\n                    \"A dashboard already exists and user doesn't have \"\n                    \"permissions to restore it\"\n                )","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/dashboard/importers/v1/utils.py#L319-L355","documentation":"ImportFailedError('Dashboard was deleted and re-import requires can_write permission to restore it') is raised in the v1 import path when the bundle's dashboard UUID matches a soft-deleted dashboard row (deleted_at set) and the importer lacks can_write (import permission). Re-importing a soft-deleted UUID is treated as an implicit restore, which is a write-level operation, so it is refused rather than silently reviving the row.","triggerScenarios":"Importing a v1 dashboard ZIP whose dashboard UUID corresponds to a dashboard that was soft-deleted in the target instance, using an account (or token) without dashboard write/import permission.","commonSituations":"Re-importing an old export after the dashboard was deleted during cleanup; automated sync pipelines that push bundles with a viewer-level service account; deleted dashboards in the trash-like soft-delete state blocking re-import for non-writers.","solutions":["Run the import with a user/role that has dashboard write permission (can_write / can_import on Dashboard).","Or purge the soft-deleted dashboard first (hard-delete via the API's purge path or admin) so the UUID is free and the import becomes a plain create — still requiring create permission.","If the dashboard should stay deleted, remove it from the bundle or change its UUID before import."],"exampleFix":"# before\nclient.post('/api/v1/dashboard/import/', ...)  # viewer token, UUID soft-deleted\n\n# after\n# 1) purge the soft-deleted row as admin, or\n# 2) re-run import with a role that has can_write on Dashboard\nadmin_client.post('/api/v1/dashboard/import/', files=...)","handlingStrategy":"try-catch","validationCode":"from superset.commands.dashboard.importers.v1.utils import find_existing_for_import\nfrom superset.models.dashboard import Dashboard\n\nexisting = find_existing_for_import(Dashboard, config['uuid'])\nif existing is not None and existing.deleted_at is not None:\n    if not security_manager.can_access('can_write', 'Dashboard'):\n        raise PermissionError('re-import of soft-deleted UUID needs can_write')","typeGuard":null,"tryCatchPattern":"try:\n    run_import(bundle)\nexcept ImportFailedError as ex:\n    if 'requires can_write permission to restore' in str(ex):\n        escalate_to_writer_or_purge_soft_deleted_row()","preventionTips":["Run restore-by-import with roles holding Dashboard can_write.","Hard-delete (purge) dashboards you never intend to restore, to free the UUID.","Track which bundle UUIDs correspond to soft-deleted rows in target environments."],"tags":["security","rbac","import","restore","soft-delete","uuid"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}