Skip to content

Complete namespace scoping across the id-addressed storage APIs #597

Closed

Description

@stromanni

#596 scopes the dashboard listings by namespace, closing #587. Reviewing it, CodeRabbit pointed out that the id-addressed and recovery paths beside them are still unscoped — so a caller holding an id from another namespace can still read or mutate that record.

The listings were the reported hole and are fixed. These are the rest of the surface, filed together because they share one root cause and one shape of fix: the Storage trait method takes no namespace, so every one of them needs a signature change carried through traits.rs, the diesel_common macros, all three backends, the delegate! macro, and each binding that calls it. That is why they did not ride along with #596.

Still unscoped

Recovery and scheduling

  • scheduler/maintenance.rs — reap_stale_jobs(now) and reap_orphaned_jobs(&live, now) take no namespace, so a scheduler can retry or dead-letter another namespace's job, and handle_result then records the metric under its own namespace.
  • scheduler/poller.rs — count_running_by_task counts across namespaces, so a job elsewhere consumes this scheduler's max_concurrent budget and causes a wrong reschedule.

Id-addressed reads and mutations

  • storage/diesel_common/jobs.rs — cancel_job checks the namespace, then calls cascade_cancel(failed_job_id, reason), which archives every pending dependent without one. Dependency validation does not require dependencies to share a namespace, so a cancel in one namespace can cancel a dependent in another.
  • storage/diesel_common/logs.rs — get_task_logs / get_task_logs_after take a job id and no namespace. taskito-server's job-log route and the Node bindings both expose them.
  • taskito-node/src/queue/mod.rs — get_job, request_cancel, update_progress still take a bare id.
  • taskito-node/src/queue/inspect.rs — list_archived / list_archived_after call unscoped storage APIs, so a namespaced queue lists every namespace's archive.

Workflow state

  • taskito-java/src/workflows/mod.rs and taskito-node/src/queue/workflows.rs — several paths load a run or node by an unscoped id and then mark it skipped, failed or cancelled, or ignore the false that cancel_job returns on a namespace mismatch. A caller with a valid identifier can mutate workflow state owned by another namespace.

Shape of the fix

For each: add namespace: Option<&str> to the storage method, apply it as an equality filter, and return the same not-found answer on a mismatch that cancel_job already gives — a caller scoped to one tenant learns nothing about ids outside it. Then forward the queue namespace from every binding.

Worth deciding once, up front: whether cross-namespace job dependencies are a supported contract. If they are not, enqueue should reject them, which makes the cascade_cancel fix a filter rather than a behaviour change.

Done when

A caller scoped to namespace A, holding a valid id from namespace B, gets not-found from every read and no effect from every mutation — asserted per path, plus a recovery test proving a scheduler in A never reaps, retries or dead-letters a job in B.

Context

Raised by CodeRabbit on #596; the individual review threads carry its per-path analysis.

Activity

  1. pratyush618 commented on Aug 2, 2026

    @pratyush618
    Contributor

    Fixed in #606 (`ea93827a`).

    Fourteen Storage methods now take namespace: Option<&str> — get_job, request_cancel, is_cancel_requested, mark_cancelled, update_progress, cascade_cancel, get_job_errors, get_task_logs(_after), list_archived(_after), count_running_by_task, reap_stale_jobs, reap_orphaned_jobs. A mismatch gets the same answer an unknown id gets: missing on a read, no effect on a mutation. None still addresses every namespace.

    The done-when holds: 19 tests in namespace_scoping.rs cover every read reporting not-found and every mutation having no effect, plus a scheduler in A never reaping or timing out a job in B.

    Two things from this issue landed beyond the storage signatures. CancelSignals and the executor side-channel now carry the namespace — neither had it, and both address jobs by id. Review also caught a disclosure the signature change alone did not close: the DAG walks pushed an edge per dependency id before looking the adjacent job up, so scoping get_job skipped the foreign node but still shipped its id. All four walks now keep an edge only once both endpoints resolved.

    Not closed here, tracked in #608: cross-namespace dependency policy (get_dependencies/get_dependents), the result-path mutations, and workflow-run scoping. The workflow job-bridge half — workflow code calling get_job/request_cancel without a namespace — was fixed here; it needed no schema change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions