WES: use standard workflow accessibility check for gxworkflow:// refs

Ownership-only check rejected published/shared workflows that a normal
invocation would accept. Defer to get_stored_accessible_workflow, update
docs, add API tests for published + inaccessible cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
John Chilton
2026-07-21 08:16:48 -04:00
co-authored by Claude Opus 4.8
parent 1981c78e84
commit e4e29c74e1
3 changed files with 58 additions and 26 deletions
+15 -13
View File
@@ -129,16 +129,16 @@ Trimmed to the WES-relevant fields (the real response also carries
A WES `RunRequest` is `multipart/form-data`. The fields Galaxy honors:
| Field | Required | Notes |
| --------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `workflow_type` | yes | `gx_workflow_format2` or `gx_workflow_ga`. Must match the auto-detected type or you get a 400. |
| `workflow_type_version` | yes | Free-form string, e.g. `"1.0.0"`. |
| `workflow_url` | one of url/attachment | A URL Galaxy can fetch (`http(s)`, `s3`, `gs`, `file`, `base64://`) **or** a `gxworkflow://` reference (see below). |
| `workflow_attachment` | one of url/attachment | The workflow file uploaded inline. |
| `workflow_params` | no | JSON object of workflow inputs (see below). |
| `workflow_engine_parameters` | no | JSON object of Galaxy-specific run options (see below). |
| `tags` | no | Accepted but currently not persisted onto the invocation. |
| `workflow_engine` / `workflow_engine_version` | no | Accepted; informational. |
| Field | Required | Notes |
| --------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `workflow_type` | yes | `gx_workflow_format2` or `gx_workflow_ga`. Must match the auto-detected type or you get a 400. |
| `workflow_type_version` | yes | Free-form string, e.g. `"1.0.0"`. |
| `workflow_url` | one of url/attachment | A URL Galaxy can fetch (`http(s)://`, `s3://`, `gs://`, `file://`, `base64://`) **or** a `gxworkflow://` reference (see below). |
| `workflow_attachment` | one of url/attachment | The workflow file uploaded inline. |
| `workflow_params` | no | JSON object of workflow inputs (see below). |
| `workflow_engine_parameters` | no | JSON object of Galaxy-specific run options (see below). |
| `tags` | no | Accepted but currently not persisted onto the invocation. |
| `workflow_engine` / `workflow_engine_version` | no | Accepted; informational. |
### `workflow_params` — wiring up inputs
@@ -180,7 +180,9 @@ gxworkflow://<encoded_workflow_id> # the StoredWorkflow (latest versi
gxworkflow://<encoded_workflow_id>?instance=true # a specific Workflow instance
```
The caller must own the workflow (or be an admin). With `gxworkflow://`, Galaxy skips
The workflow just has to be accessible to the caller — the same rule a normal invocation
uses: owned by them, shared with them, published/importable, or the caller is an admin.
Otherwise you get a 403. With `gxworkflow://`, Galaxy skips
import and invokes the stored workflow directly. `workflow_type` is still required by the
form but is **not** validated against the stored workflow in this case — the
"must match the auto-detected type or 400" check only applies to inline
@@ -400,8 +402,8 @@ Access to resources you do not own is **not** reported uniformly — watch for t
- Reading a **run** you don't own → `403` (`AuthenticationRequired`).
- Submitting against a **history** you don't own → `404` (`ObjectNotFound`).
- A `gxworkflow://` reference to a **workflow** you don't own → `403`
(`ItemAccessibilityException`).
- A `gxworkflow://` reference to a **workflow** you cannot access (not owned, not shared
with you, not published) → `403` (`ItemAccessibilityException`).
So a `404` on submission can mean "your history id is wrong" _or_ "that history belongs to
someone else"; don't assume `404` always means the object is absent.
+5 -13
View File
@@ -483,22 +483,14 @@ class WesService(ServiceBase):
workflow_uri = workflow_dict["workflow_uri"]
encoded_workflow_id, instance = _parse_gxworkflow_uri(workflow_uri)
# Load the workflow from the database
# Load the workflow from the database, applying the same accessibility
# rules as a normal invocation (owned, shared, published, or admin).
# by_stored_id=not instance means:
# - False (instance=False) -> load StoredWorkflow (by_stored_id=True)
# - True (instance=True) -> load Workflow (by_stored_id=False)
try:
stored_workflow = self._workflows_service._workflows_manager.get_stored_workflow(
trans, encoded_workflow_id, by_stored_id=not instance
)
except Exception as e:
raise exceptions.ObjectNotFound(
f"Workflow '{encoded_workflow_id}' not found or not accessible: {str(e)}"
)
# Validate user has access to this workflow
if stored_workflow.user_id != trans.user.id and not trans.user_is_admin:
raise exceptions.ItemAccessibilityException("You do not have access to this workflow")
stored_workflow = self._workflows_service._workflows_manager.get_stored_accessible_workflow(
trans, encoded_workflow_id, by_stored_id=not instance
)
# Use the existing workflow directly - no need to create a new one
# Skip to step 5 (engine parameters and history)
+38
View File
@@ -546,6 +546,44 @@ steps:
# Validate response
assert run_id is not None
def test_wes_submit_run_with_gxworkflow_uri_published_workflow(self):
"""A published workflow owned by another user can be run via gxworkflow://."""
with self._different_user():
workflow_id = self._upload_yaml_workflow(WORKFLOW_SIMPLE)
self.workflow_populator.make_public(workflow_id)
with self.dataset_populator.test_history() as history_id:
dataset_id = self._get_test_dataset_id(history_id)
data = {
"workflow_type": "gx_workflow_ga",
"workflow_type_version": "v1",
"workflow_params": json.dumps({"input1": dataset_id}),
"workflow_url": f"gxworkflow://{workflow_id}",
}
response = self._wes_post("ga4gh/wes/v1/runs", data=data)
self._assert_status_code_is(response, 200)
assert response.json()["run_id"] is not None
def test_wes_submit_run_with_gxworkflow_uri_inaccessible_workflow(self):
"""An unshared workflow owned by another user cannot be run via gxworkflow://."""
with self._different_user():
workflow_id = self._upload_yaml_workflow(WORKFLOW_SIMPLE)
with self.dataset_populator.test_history() as history_id:
dataset_id = self._get_test_dataset_id(history_id)
data = {
"workflow_type": "gx_workflow_ga",
"workflow_type_version": "v1",
"workflow_params": json.dumps({"input1": dataset_id}),
"workflow_url": f"gxworkflow://{workflow_id}",
}
response = self._wes_post("ga4gh/wes/v1/runs", data=data)
self._assert_status_code_is(response, 403)
def test_wes_job_stdout_endpoint(self):
"""Test /api/jobs/{job_id}/stdout endpoint returns job stdout."""
with self.dataset_populator.test_history() as history_id: