{"record":{"id":"a991483a17333b17","repo":"apache/superset","slug":"a-dashboard-already-exists-and-user-doesn-t-have-p","errorCode":null,"errorMessage":"A dashboard already exists and user doesn't have permissions to restore it","messagePattern":"A dashboard already exists and user doesn't have permissions to restore it","errorType":"exception","errorClass":"ImportFailedError","httpStatus":500,"severity":"error","filePath":"superset/commands/dashboard/importers/v1/utils.py","lineNumber":352,"sourceCode":"                # 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                )\n            # Restore in place (clear ``deleted_at``) rather than\n            # hard-delete-and-replace: a hard delete would cascade through\n            # dashboard_slices junctions and editor / viewer / tag\n            # associations, breaking the relationships the import would then\n            # need to reconstruct.\n            #\n            # How the restore lands as an UPDATE: clearing\n            # ``existing.deleted_at`` marks the in-session row dirty and the\n            # explicit flush emits the ``deleted_at = NULL`` UPDATE before\n            # ``Dashboard.import_from_dict`` (below) does its own query-by-uuid\n            # lookup. Without the flush we would rely on autoflush ahead of\n            # that internal query — correct under default session config but a\n            # hidden contract; the explicit flush makes it robust. The lookup\n            # then finds the now-live row (the listener filters\n            # ``deleted_at IS NULL``) and ``import_from_dict`` applies the","sourceCodeStart":334,"sourceCodeEnd":370,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/commands/dashboard/importers/v1/utils.py#L334-L370","documentation":"ImportFailedError('A dashboard already exists and user doesn't have permissions to restore it') is raised in the v1 import restore branch when the user has can_write but fails the object-level checks on the soft-deleted dashboard: they cannot access it (can_access_dashboard false) or are neither its editor nor an admin. Restore-with-update touches an existing object, so editorship of that specific row is required.","triggerScenarios":"Importing a v1 bundle whose UUID matches a soft-deleted dashboard, with a user who has generic import permission but no access to / editorship of that particular dashboard (e.g. it was owned by a departed user or a service account).","commonSituations":"Re-importing team dashboards after the original owner left; ownership left on a service account so no human passes is_editor; multi-tenant instances where the deleted dashboard belongs to another team.","solutions":["Have an admin perform the import (is_admin() passes the check).","Or transfer ownership of the soft-deleted dashboard to the importing user (update owners via admin API), then retry.","Or purge the soft-deleted row entirely so the import creates a fresh dashboard under the caller's ownership."],"exampleFix":"# before\nclient.post('/api/v1/dashboard/import/', ...)  # has can_write but not editor of deleted dash\n\n# after\n# admin transfers ownership or performs the import\nadmin_client.put(f'/api/v1/dashboard/{id}', json={'owners': [importer_user_id]})\nclient.post('/api/v1/dashboard/import/', files=...)","handlingStrategy":"try-catch","validationCode":"existing = find_existing_for_import(Dashboard, config['uuid'])\nuser = get_user()\nif existing is not None and existing.deleted_at is not None and user:\n    if not security_manager.can_access_dashboard(existing) or (\n        not security_manager.is_editor(existing)\n        and not security_manager.is_admin()\n    ):\n        raise PermissionError('restore needs admin or editorship of the deleted dashboard')","typeGuard":null,"tryCatchPattern":"try:\n    run_import(bundle)\nexcept ImportFailedError as ex:\n    if \"doesn't have permissions to restore\" in str(ex):\n        transfer_ownership_then_retry() or run_as_admin()","preventionTips":["Have admins perform restores of dashboards owned by departed users.","Keep ownership of important dashboards on a team role, not individuals.","Purge rather than soft-delete when the UUID must be freely re-importable."],"tags":["security","rbac","import","restore","editorship","soft-delete"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}