Skip to content

Commit 98c5c34

Browse files
authored
Merge pull request #20949 from guerler/relax_admin_requirements_for_tool_data
Limit admin requirement of selected tool data api endpoints
2 parents ca8b1b4 + 100a669 commit 98c5c34

File tree

4 files changed

+75
-29
lines changed

4 files changed

+75
-29
lines changed

client/src/api/schema/schema.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4584,7 +4584,7 @@ export interface paths {
45844584
cookie?: never;
45854585
};
45864586
/**
4587-
* Get details of a given data table
4587+
* Get details of a data table. For non-administrators, base directories in the path column are stripped, leaving only the basename.
45884588
* @description Get details of a given tool data table.
45894589
*/
45904590
get: operations["show_api_tool_data__table_name__get"];
@@ -4609,7 +4609,7 @@ export interface paths {
46094609
};
46104610
/**
46114611
* Get information about a particular field in a tool data table
4612-
* @description Reloads a data table and return its details.
4612+
* @description Displays information about a data table field.
46134613
*/
46144614
get: operations["show_field_api_tool_data__table_name__fields__field_name__get"];
46154615
put?: never;
@@ -4628,7 +4628,7 @@ export interface paths {
46284628
cookie?: never;
46294629
};
46304630
/**
4631-
* Get information about a particular field in a tool data table
4631+
* Get files associated with a particular field in a tool data table
46324632
* @description Download a file associated with the data table field.
46334633
*/
46344634
get: operations["download_field_file_api_tool_data__table_name__fields__field_name__files__file_name__get"];
@@ -38069,7 +38069,7 @@ export interface operations {
3806938069
};
3807038070
requestBody?: never;
3807138071
responses: {
38072-
/** @description A description of the given data table and its content */
38072+
/** @description A description of the given data table and its content. */
3807338073
200: {
3807438074
headers: {
3807538075
[name: string]: unknown;
@@ -38211,7 +38211,7 @@ export interface operations {
3821138211
};
3821238212
requestBody?: never;
3821338213
responses: {
38214-
/** @description Information about a data table field */
38214+
/** @description Request file associated with tool data table entry */
3821538215
200: {
3821638216
headers: {
3821738217
[name: string]: unknown;

lib/galaxy/managers/tool_data.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from os.path import basename
12
from pathlib import Path
23
from typing import (
34
cast,
@@ -7,6 +8,7 @@
78
from galaxy import exceptions
89
from galaxy.files import ConfiguredFileSources
910
from galaxy.files.uris import stream_url_to_file
11+
from galaxy.managers.context import ProvidesUserContext
1012
from galaxy.model import DatasetInstance
1113
from galaxy.structured_app import (
1214
MinimalManagerApp,
@@ -27,6 +29,9 @@
2729
ToolDataTable,
2830
)
2931

32+
# Tables that are accessible without admin rights
33+
PUBLIC_TABLES = ["fasta_indexes", "twobit"]
34+
3035

3136
class ToolDataManager:
3237
"""
@@ -44,14 +49,21 @@ def index(self) -> ToolDataEntryList:
4449
"""Return all tool data tables."""
4550
return self._app.tool_data_tables.index()
4651

47-
def show(self, table_name: str) -> ToolDataDetails:
52+
def show(self, trans: ProvidesUserContext, table_name: str) -> ToolDataDetails:
4853
"""Get details of a given data table"""
4954
data_table = self._data_table(table_name)
5055
element_view = data_table.to_dict(view="element")
56+
if not trans.user_is_admin:
57+
path_index = element_view["columns"].index("path") if "path" in element_view["columns"] else None
58+
if path_index is not None:
59+
element_view["fields"] = [
60+
[basename(field[path_index]) if i == path_index else field[i] for i in range(len(field))]
61+
for field in element_view["fields"]
62+
]
5163
return ToolDataDetails.model_construct(**element_view)
5264

5365
def show_field(self, table_name: str, field_name: str) -> ToolDataField:
54-
"""Get information about a partiular field in a tool data table"""
66+
"""Get information about a particular field in a tool data table"""
5567
field = self._data_table_field(table_name, field_name)
5668
return ToolDataField.model_construct(**field.to_dict())
5769

@@ -61,9 +73,13 @@ def reload(self, table_name: str) -> ToolDataDetails:
6173
data_table.reload_from_files()
6274
return self._reload_data_table(table_name)
6375

64-
def get_field_file_path(self, table_name: str, field_name: str, file_name: str) -> Path:
76+
def get_field_file_path(self, trans, table_name: str, field_name: str, file_name: str) -> Path:
6577
"""Get the absolute path to a given file name in the table field"""
6678
field_value = self._data_table_field(table_name, field_name)
79+
if table_name not in PUBLIC_TABLES and not trans.user_is_admin:
80+
raise exceptions.AdminRequiredException(
81+
f"Only administrators can download files from data table {table_name}."
82+
)
6783
base_dir = Path(field_value.get_base_dir())
6884
full_path = base_dir / file_name
6985
if str(full_path) not in field_value.get_files():
@@ -101,9 +117,13 @@ def _data_table_field(self, table_name: str, field_name: str) -> TabularToolData
101117
raise exceptions.ObjectNotFound(f"No such field {field_name} in data table {table_name}.")
102118
return out
103119

104-
def _reload_data_table(self, name: str) -> ToolDataDetails:
105-
self._app.queue_worker.send_control_task("reload_tool_data_tables", noop_self=True, kwargs={"table_name": name})
106-
return self.show(name)
120+
def _reload_data_table(self, table_name: str) -> ToolDataDetails:
121+
self._app.queue_worker.send_control_task(
122+
"reload_tool_data_tables", noop_self=True, kwargs={"table_name": table_name}
123+
)
124+
data_table = self._data_table(table_name)
125+
element_view = data_table.to_dict(view="element")
126+
return ToolDataDetails.model_construct(**element_view)
107127

108128

109129
class ToolDataImportManager:

lib/galaxy/webapps/galaxy/api/tool_data.py

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111

1212
from galaxy.celery.tasks import import_data_bundle
13+
from galaxy.managers.context import ProvidesUserContext
1314
from galaxy.managers.tool_data import ToolDataManager
1415
from galaxy.schema.schema import (
1516
AsyncTaskResultSummary,
@@ -25,6 +26,7 @@
2526
from galaxy.webapps.galaxy.services.base import async_task_summary
2627
from . import (
2728
depends,
29+
DependsOnTrans,
2830
Router,
2931
)
3032

@@ -43,6 +45,12 @@
4345
description="The name of the tool data table field",
4446
)
4547

48+
ToolDataTableFieldFileName = Path(
49+
...,
50+
title="File name",
51+
description="The name of a file associated with this data table field",
52+
)
53+
4654

4755
class ImportToolDataBundle(BaseModel):
4856
source: ImportToolDataBundleSource = Field(..., discriminator="src")
@@ -77,13 +85,15 @@ def create(
7785

7886
@router.get(
7987
"/api/tool_data/{table_name}",
80-
summary="Get details of a given data table",
81-
response_description="A description of the given data table and its content",
82-
require_admin=True,
88+
summary="Get details of a data table. For non-administrators, base directories in the path column are stripped, leaving only the basename.",
89+
response_description="A description of the given data table and its content.",
90+
public=True,
8391
)
84-
async def show(self, table_name: str = ToolDataTableName) -> ToolDataDetails:
92+
async def show(
93+
self, trans: ProvidesUserContext = DependsOnTrans, table_name: str = ToolDataTableName
94+
) -> ToolDataDetails:
8595
"""Get details of a given tool data table."""
86-
return self.tool_data_manager.show(table_name)
96+
return self.tool_data_manager.show(trans, table_name)
8797

8898
@router.get(
8999
"/api/tool_data/{table_name}/reload",
@@ -106,28 +116,25 @@ async def show_field(
106116
table_name: str = ToolDataTableName,
107117
field_name: str = ToolDataTableFieldName,
108118
) -> ToolDataField:
109-
"""Reloads a data table and return its details."""
119+
"""Displays information about a data table field."""
110120
return self.tool_data_manager.show_field(table_name, field_name)
111121

112122
@router.get(
113123
"/api/tool_data/{table_name}/fields/{field_name}/files/{file_name}",
114-
summary="Get information about a particular field in a tool data table",
115-
response_description="Information about a data table field",
124+
summary="Get files associated with a particular field in a tool data table",
125+
response_description="Request file associated with tool data table entry",
116126
response_class=GalaxyFileResponse,
117-
require_admin=True,
127+
public=True,
118128
)
119129
def download_field_file(
120130
self,
131+
trans: ProvidesUserContext = DependsOnTrans,
121132
table_name: str = ToolDataTableName,
122133
field_name: str = ToolDataTableFieldName,
123-
file_name: str = Path(
124-
..., # Mark this field as required
125-
title="File name",
126-
description="The name of a file associated with this data table field",
127-
),
134+
file_name: str = ToolDataTableFieldFileName,
128135
):
129136
"""Download a file associated with the data table field."""
130-
path = self.tool_data_manager.get_field_file_path(table_name, field_name, file_name)
137+
path = self.tool_data_manager.get_field_file_path(trans, table_name, field_name, file_name)
131138
return GalaxyFileResponse(str(path))
132139

133140
@router.delete(

lib/galaxy_test/api/test_tool_data.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,29 @@ def test_admin_only(self):
1919
def test_list(self):
2020
index_response = self._get("tool_data", admin=True)
2121
self._assert_status_code_is(index_response, 200)
22-
print(index_response.content)
2322
index = index_response.json()
2423
assert "testalpha" in [operator.itemgetter("name")(_) for _ in index]
2524

2625
def test_show(self):
2726
show_response = self._get("tool_data/testalpha", admin=True)
2827
self._assert_status_code_is(show_response, 200)
29-
print(show_response.content)
3028
data_table = show_response.json()
3129
assert data_table["columns"] == ["value", "name", "path"]
3230
first_entry = data_table["fields"][0]
3331
assert first_entry[0] == "data1"
3432
assert first_entry[1] == "data1name"
3533
assert first_entry[2].endswith("test/functional/tool-data/data1/entry.txt")
3634

35+
def test_show_anon(self):
36+
show_response = self._get("tool_data/testalpha")
37+
self._assert_status_code_is(show_response, 200)
38+
data_table = show_response.json()
39+
assert data_table["columns"] == ["value", "name", "path"]
40+
first_entry = data_table["fields"][0]
41+
assert first_entry[0] == "data1"
42+
assert first_entry[1] == "data1name"
43+
assert first_entry[2] == "entry.txt"
44+
3745
def test_show_field(self):
3846
show_field_response = self._get("tool_data/testalpha/fields/data1", admin=True)
3947
self._assert_status_code_is(show_field_response, 200)
@@ -48,10 +56,21 @@ def test_download_field_file(self):
4856
content = show_field_response.text
4957
assert content == "This is data 1.", content
5058

59+
def test_download_field_file_anon_raises_404(self):
60+
show_field_response = self._get("tool_data/twobit/fields/data1/files/entry.txt")
61+
self._assert_status_code_is(show_field_response, 404)
62+
err_msg = show_field_response.json()["err_msg"]
63+
assert err_msg == "No such field data1 in data table twobit."
64+
65+
def test_download_field_file_anon_raises_403(self):
66+
show_field_response = self._get("tool_data/testalpha/fields/data1/files/entry.txt")
67+
self._assert_status_code_is(show_field_response, 403)
68+
err_msg = show_field_response.json()["err_msg"]
69+
assert err_msg == "Only administrators can download files from data table testalpha."
70+
5171
def test_reload(self):
5272
show_response = self._get("tool_data/test_fasta_indexes/reload", admin=True)
5373
self._assert_status_code_is(show_response, 200)
54-
print(show_response.content)
5574
data_table = show_response.json()
5675
assert data_table["columns"] == ["value", "dbkey", "name", "path"]
5776

0 commit comments

Comments
 (0)