{"record":{"id":"92e07b7585475122","repo":"apache/superset","slug":"not-authorized-to-cancel-this-job","errorCode":null,"errorMessage":"Not authorized to cancel this job","messagePattern":"Not authorized to cancel this job","errorType":"http","errorClass":"AsyncQueryTokenException","httpStatus":403,"severity":"error","filePath":"superset/async_events/async_query_manager.py","lineNumber":441,"sourceCode":"        owner. The terminal ``STATUS_CANCELLED`` event is emitted here rather\n        than by the worker, which never runs for a task revoked while it was\n        still queued; the worker only logs the cancellation it is told about\n        through the flag, so a job still gets exactly one terminal event.\n\n        :raises AsyncQueryJobException: the job is unknown or already terminal\n        :raises AsyncQueryTokenException: the caller does not own the job\n        \"\"\"\n        if not self._cache:\n            raise CacheBackendNotInitialized(\"Cache backend not initialized\")\n\n        key = self._job_registry_key(job_id)\n        raw = self._cache.get(key)\n        if raw is None:\n            raise AsyncQueryJobException(\"Job not found or already completed\")\n\n        record = json.loads(raw)\n        if record.get(\"channel_id\") != channel_id or record.get(\"user_id\") != user_id:\n            raise AsyncQueryTokenException(\"Not authorized to cancel this job\")\n\n        # Flag before revoking so the worker's timeout handler, which may fire\n        # almost immediately, reliably sees the cancellation. Write only if the\n        # key still exists (``xx``): if the job finished and cleared its record\n        # between the read above and here, don't recreate a stale record or\n        # revoke a task that is already gone — report it as not found instead.\n        flagged = self._cache.set(\n            key,\n            json.dumps({**record, \"cancelled\": True}),\n            ex=self._jwt_expiration_seconds or None,\n            xx=True,\n        )\n        if not flagged:\n            raise AsyncQueryJobException(\"Job not found or already completed\")\n\n        # pylint: disable=import-outside-toplevel\n        from superset.extensions import celery_app\n","sourceCodeStart":423,"sourceCodeEnd":459,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/async_events/async_query_manager.py#L423-L459","documentation":"AsyncQueryTokenException raised during cancellation when the registry record exists but its stored channel_id or user_id does not match the caller's. Each job's cancel record is scoped to the channel and user that created it, so only the owner may cancel; a mismatch means a different user (or a different session/channel for the same user) is attempting the cancellation. This is an authorization check, not a token-format issue, despite the exception class name.","triggerScenarios":"User A calling cancel with User B's job_id (even if both are logged in); the same user cancelling from a session whose channel id differs (e.g. cookie re-issued after re-login, embedded vs standard session); a forged request replaying another channel's metadata with a guessed job_id.","commonSituations":"Multi-tab/multi-session usage where job metadata from one tab is used in another; embedded dashboard guest sessions mixing with the owner's session; frontend bugs that cache job metadata across user logins (logout then login as another user without clearing state).","solutions":["Ensure the cancel request uses the channel_id and user_id from the exact job metadata returned when the job was created — do not reconstruct them from the current session alone.","Clear cached job metadata on logout so a new user's session never reuses the previous user's channel.","Treat AsyncQueryTokenException from cancel as 403 in the API layer: log and inform the user they do not own the job."],"exampleFix":"# before\njob = load_last_job_from_localstorage()  # may belong to previous user\nmanager.cancel_job(job[\"job_id\"], current_channel_id, current_user_id)\n\n# after\njob = load_last_job_from_localstorage()\nif job and job[\"user_id\"] == current_user_id:\n    manager.cancel_job(job[\"job_id\"], job[\"channel_id\"], job[\"user_id\"])","handlingStrategy":"validation","validationCode":"job = get_job_metadata()  # exactly what the create call returned\nowns_job = (\n    job is not None\n    and job.get(\"channel_id\") == current_channel_id\n    and job.get(\"user_id\") == current_user_id\n)\nif not owns_job:\n    skip_cancel(\"not job owner\")","typeGuard":null,"tryCatchPattern":"except AsyncQueryTokenException as ex:\n    if \"Not authorized\" in str(ex):\n        return response_403()  # owner mismatch, do not retry","preventionTips":["Always cancel with the job metadata returned at creation time, not reconstructed session values.","Purge cached job metadata on logout to prevent cross-user leakage."],"tags":["async-queries","authorization","job-cancellation","multi-user"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}