diff --git a/client/src/api/schema/schema.ts b/client/src/api/schema/schema.ts index 996f9ac4f44a..2a331e2c7540 100644 --- a/client/src/api/schema/schema.ts +++ b/client/src/api/schema/schema.ts @@ -12918,7 +12918,10 @@ export interface components { * @enum {string} */ type: "aws_s3" | "azure_blob" | "boto3" | "disk" | "generic_s3"; - /** Uuid */ + /** + * Uuid + * Format: uuid4 + */ uuid: string; /** Variables */ variables: { @@ -12992,7 +12995,10 @@ export interface components { type: "ftp" | "posix" | "s3fs" | "azure"; /** Uri Root */ uri_root: string; - /** Uuid */ + /** + * Uuid + * Format: uuid4 + */ uuid: string; /** Variables */ variables: { diff --git a/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue b/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue index 22529f241078..bd1f6434f1e9 100644 --- a/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue +++ b/client/src/components/Libraries/LibraryFolder/TopToolbar/FolderTopBar.vue @@ -89,6 +89,9 @@ const canDelete = computed(() => { const datasetManipulation = computed(() => { return !!(containsFileOrFolder.value && Galaxy.user); }); +const totalRows = computed(() => { + return props.metadata?.total_rows ?? 0; +}); function updateSearch(value: string) { emit("updateSearch", value); @@ -106,7 +109,8 @@ async function getSelected() { const selected = await services.getFilteredFolderContents( props.folderId, props.unselected, - props.searchText + props.searchText, + totalRows.value ); emit("setBusy", false); diff --git a/client/src/components/Libraries/LibraryFolder/services.js b/client/src/components/Libraries/LibraryFolder/services.js index 7b59d871bbde..c94c76135eeb 100644 --- a/client/src/components/Libraries/LibraryFolder/services.js +++ b/client/src/components/Libraries/LibraryFolder/services.js @@ -30,12 +30,13 @@ export class Services { } } - async getFilteredFolderContents(id, excluded, searchText) { + async getFilteredFolderContents(id, excluded, searchText, limit) { // The intent of this method is to get folder contents applying - // seachText filters only; we explicitly set limit to 0 + // seachText filters only; limit should match the total number of + // items in the folder, so that all items are returned. const config = { params: { - limit: 0, + limit, }, }; searchText = searchText?.trim(); diff --git a/lib/galaxy/exceptions/__init__.py b/lib/galaxy/exceptions/__init__.py index 82f66efdbe7d..f8e8c956ac98 100644 --- a/lib/galaxy/exceptions/__init__.py +++ b/lib/galaxy/exceptions/__init__.py @@ -209,6 +209,11 @@ class UserCannotRunAsException(MessageException): err_code = error_codes_by_name["USER_CANNOT_RUN_AS"] +class UserRequiredException(MessageException): + status_code = 403 + err_code = error_codes_by_name["USER_REQUIRED"] + + class AdminRequiredException(MessageException): status_code = 403 err_code = error_codes_by_name["ADMIN_REQUIRED"] diff --git a/lib/galaxy/exceptions/error_codes.json b/lib/galaxy/exceptions/error_codes.json index e148b102b786..1c57fbc7bbad 100644 --- a/lib/galaxy/exceptions/error_codes.json +++ b/lib/galaxy/exceptions/error_codes.json @@ -144,6 +144,11 @@ "code": 403007, "message": "Action requires account activation." }, + { + "name": "USER_REQUIRED", + "code": 403008, + "message": "Action requires user authentication." + }, { "name": "USER_OBJECT_NOT_FOUND", "code": 404001, diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 61a32ce8b802..64ffcd29b022 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -2005,13 +2005,13 @@ def fail(message=job.info, exception=None): # Once datasets are collected, set the total dataset size (includes extra files) for dataset_assoc in job.output_datasets: dataset = dataset_assoc.dataset.dataset - if not dataset.purged: - # assume all datasets in a job get written to the same objectstore - quota_source_info = dataset.quota_source_info - collected_bytes += dataset.set_total_size() - else: + # assume all datasets in a job get written to the same objectstore + quota_source_info = dataset.quota_source_info + collected_bytes += dataset.set_total_size() + if dataset.purged: # Purge, in case job wrote directly to object store dataset.full_delete() + collected_bytes = 0 user = job.user if user and collected_bytes > 0 and quota_source_info is not None and quota_source_info.use: diff --git a/lib/galaxy/managers/file_source_instances.py b/lib/galaxy/managers/file_source_instances.py index 9b70926c8a42..f364110828cd 100644 --- a/lib/galaxy/managers/file_source_instances.py +++ b/lib/galaxy/managers/file_source_instances.py @@ -13,6 +13,7 @@ from pydantic import ( BaseModel, + UUID4, ValidationError, ) @@ -87,7 +88,7 @@ class UserFileSourceModel(BaseModel): - uuid: str + uuid: UUID4 uri_root: str name: str description: Optional[str] @@ -142,16 +143,16 @@ def index(self, trans: ProvidesUserContext) -> List[UserFileSourceModel]: stores = self._sa_session.query(UserFileSource).filter(UserFileSource.user_id == trans.user.id).all() return [self._to_model(trans, s) for s in stores] - def show(self, trans: ProvidesUserContext, uuid: str) -> UserFileSourceModel: + def show(self, trans: ProvidesUserContext, uuid: UUID4) -> UserFileSourceModel: user_file_source = self._get(trans, uuid) return self._to_model(trans, user_file_source) - def purge_instance(self, trans: ProvidesUserContext, uuid: str) -> None: + def purge_instance(self, trans: ProvidesUserContext, uuid: UUID4) -> None: persisted_file_source = self._get(trans, uuid) purge_template_instance(trans, persisted_file_source, self._app_config) def modify_instance( - self, trans: ProvidesUserContext, id: str, payload: ModifyInstancePayload + self, trans: ProvidesUserContext, id: UUID4, payload: ModifyInstancePayload ) -> UserFileSourceModel: if isinstance(payload, UpgradeInstancePayload): return self._upgrade_instance(trans, id, payload) @@ -162,7 +163,7 @@ def modify_instance( return self._update_instance(trans, id, payload) def _upgrade_instance( - self, trans: ProvidesUserContext, id: str, payload: UpgradeInstancePayload + self, trans: ProvidesUserContext, id: UUID4, payload: UpgradeInstancePayload ) -> UserFileSourceModel: persisted_file_source = self._get(trans, id) template = self._get_template(persisted_file_source, payload.template_version) @@ -181,7 +182,7 @@ def _upgrade_instance( return self._to_model(trans, persisted_file_source) def _update_instance( - self, trans: ProvidesUserContext, id: str, payload: UpdateInstancePayload + self, trans: ProvidesUserContext, id: UUID4, payload: UpdateInstancePayload ) -> UserFileSourceModel: persisted_file_source = self._get(trans, id) template = self._get_template(persisted_file_source) @@ -189,7 +190,7 @@ def _update_instance( return self._to_model(trans, persisted_file_source) def _update_instance_secret( - self, trans: ProvidesUserContext, id: str, payload: UpdateInstanceSecretPayload + self, trans: ProvidesUserContext, id: UUID4, payload: UpdateInstanceSecretPayload ) -> UserFileSourceModel: persisted_file_source = self._get(trans, id) template = self._get_template(persisted_file_source) @@ -300,10 +301,10 @@ def _connection_status( exception = e return file_source, connection_exception_to_status("file source", exception) - def _index_filter(self, uuid: str): + def _index_filter(self, uuid: UUID4): return UserFileSource.__table__.c.uuid == uuid - def _get(self, trans: ProvidesUserContext, uuid: str) -> UserFileSource: + def _get(self, trans: ProvidesUserContext, uuid: UUID4) -> UserFileSource: filter = self._index_filter(uuid) user_file_source = self._sa_session.query(UserFileSource).filter(filter).one_or_none() if user_file_source is None: diff --git a/lib/galaxy/managers/object_store_instances.py b/lib/galaxy/managers/object_store_instances.py index ca1df59912a2..b0163c7be249 100644 --- a/lib/galaxy/managers/object_store_instances.py +++ b/lib/galaxy/managers/object_store_instances.py @@ -16,6 +16,8 @@ ) from uuid import uuid4 +from pydantic import UUID4 + from galaxy.exceptions import ( ItemOwnershipException, RequestParameterInvalidException, @@ -73,7 +75,7 @@ class UserConcreteObjectStoreModel(ConcreteObjectStoreModel): - uuid: str + uuid: UUID4 type: ObjectStoreTemplateType template_id: str template_version: int @@ -106,7 +108,7 @@ def summaries(self) -> ObjectStoreTemplateSummaries: return self._catalog.summaries def modify_instance( - self, trans: ProvidesUserContext, id: str, payload: ModifyInstancePayload + self, trans: ProvidesUserContext, id: UUID4, payload: ModifyInstancePayload ) -> UserConcreteObjectStoreModel: if isinstance(payload, UpgradeInstancePayload): return self._upgrade_instance(trans, id, payload) @@ -116,12 +118,12 @@ def modify_instance( assert isinstance(payload, UpdateInstancePayload) return self._update_instance(trans, id, payload) - def purge_instance(self, trans: ProvidesUserContext, id: str) -> None: + def purge_instance(self, trans: ProvidesUserContext, id: UUID4) -> None: persisted_object_store = self._get(trans, id) purge_template_instance(trans, persisted_object_store, self._app_config) def _upgrade_instance( - self, trans: ProvidesUserContext, id: str, payload: UpgradeInstancePayload + self, trans: ProvidesUserContext, id: UUID4, payload: UpgradeInstancePayload ) -> UserConcreteObjectStoreModel: persisted_object_store = self._get(trans, id) template = self._get_template(persisted_object_store, payload.template_version) @@ -140,7 +142,7 @@ def _upgrade_instance( return self._to_model(trans, persisted_object_store) def _update_instance( - self, trans: ProvidesUserContext, id: str, payload: UpdateInstancePayload + self, trans: ProvidesUserContext, id: UUID4, payload: UpdateInstancePayload ) -> UserConcreteObjectStoreModel: persisted_object_store = self._get(trans, id) template = self._get_template(persisted_object_store) @@ -148,7 +150,7 @@ def _update_instance( return self._to_model(trans, persisted_object_store) def _update_instance_secret( - self, trans: ProvidesUserContext, id: str, payload: UpdateInstanceSecretPayload + self, trans: ProvidesUserContext, id: UUID4, payload: UpdateInstanceSecretPayload ) -> UserConcreteObjectStoreModel: persisted_object_store = self._get(trans, id) template = self._get_template(persisted_object_store) @@ -200,14 +202,14 @@ def index(self, trans: ProvidesUserContext) -> List[UserConcreteObjectStoreModel stores = self._sa_session.query(UserObjectStore).filter(UserObjectStore.user_id == trans.user.id).all() return [self._to_model(trans, s) for s in stores] - def show(self, trans: ProvidesUserContext, id: str) -> UserConcreteObjectStoreModel: + def show(self, trans: ProvidesUserContext, id: UUID4) -> UserConcreteObjectStoreModel: user_object_store = self._get(trans, id) return self._to_model(trans, user_object_store) def _save(self, persisted_object_store: UserObjectStore) -> None: save_template_instance(self._sa_session, persisted_object_store) - def _get(self, trans: ProvidesUserContext, id: str) -> UserObjectStore: + def _get(self, trans: ProvidesUserContext, id: UUID4) -> UserObjectStore: filter = self._index_filter(id) user_object_store = self._sa_session.query(UserObjectStore).filter(filter).one_or_none() if user_object_store is None: @@ -274,7 +276,7 @@ def _connection_status( exception = e return object_store, connection_exception_to_status("storage location", exception) - def _index_filter(self, uuid: str): + def _index_filter(self, uuid: UUID4): return UserObjectStore.__table__.c.uuid == uuid def _get_template( diff --git a/lib/galaxy/model/store/__init__.py b/lib/galaxy/model/store/__init__.py index 59438bd5d920..d093e3c24838 100644 --- a/lib/galaxy/model/store/__init__.py +++ b/lib/galaxy/model/store/__init__.py @@ -668,9 +668,9 @@ def handle_dataset_object_edit(dataset_instance, dataset_attrs): assert file_source_root dataset_extra_files_path = os.path.join(file_source_root, dataset_extra_files_path) persist_extra_files(self.object_store, dataset_extra_files_path, dataset_instance) - # Don't trust serialized file size - dataset_instance.dataset.file_size = None - dataset_instance.dataset.set_total_size() # update the filesize record in the database + # Only trust file size if the dataset is purged. If we keep the data we should check the file size. + dataset_instance.dataset.file_size = None + dataset_instance.dataset.set_total_size() # update the filesize record in the database if dataset_instance.deleted: dataset_instance.dataset.deleted = True diff --git a/lib/galaxy/webapps/galaxy/api/__init__.py b/lib/galaxy/webapps/galaxy/api/__init__.py index 464d4bc90f41..3b96292f955d 100644 --- a/lib/galaxy/webapps/galaxy/api/__init__.py +++ b/lib/galaxy/webapps/galaxy/api/__init__.py @@ -68,6 +68,7 @@ from galaxy.exceptions import ( AdminRequiredException, UserCannotRunAsException, + UserRequiredException, ) from galaxy.managers.session import GalaxySessionManager from galaxy.managers.users import UserManager @@ -183,6 +184,17 @@ def get_user( return api_user +def get_required_user( + galaxy_session=cast(Optional[model.GalaxySession], Depends(get_session)), + api_user=cast(Optional[User], Depends(get_api_user)), +) -> User: + if galaxy_session and (user := galaxy_session.user): + return user + if api_user: + return api_user + raise UserRequiredException + + class UrlBuilder: def __init__(self, request: Request): self.request = request @@ -310,7 +322,7 @@ def set_cookie( ) -DependsOnUser = cast(Optional[User], Depends(get_user)) +DependsOnUser = cast(User, Depends(get_required_user)) def get_current_history_from_session(galaxy_session: Optional[model.GalaxySession]) -> Optional[model.History]: @@ -485,6 +497,7 @@ def cbv(self): class Router(FrameworkRouter): admin_user_dependency = AdminUserRequired + user_dependency = DependsOnUser class APIContentTypeRoute(APIRoute): diff --git a/lib/galaxy/webapps/galaxy/api/file_sources.py b/lib/galaxy/webapps/galaxy/api/file_sources.py index ef777b0e3283..957da9b54092 100644 --- a/lib/galaxy/webapps/galaxy/api/file_sources.py +++ b/lib/galaxy/webapps/galaxy/api/file_sources.py @@ -7,6 +7,7 @@ Response, status, ) +from pydantic import UUID4 from galaxy.files.templates import FileSourceTemplateSummaries from galaxy.managers.context import ProvidesUserContext @@ -16,10 +17,12 @@ ModifyInstancePayload, UserFileSourceModel, ) +from galaxy.model import User from galaxy.util.config_templates import PluginStatus from . import ( depends, DependsOnTrans, + DependsOnUser, Router, ) @@ -28,7 +31,7 @@ router = Router(tags=["file_sources"]) -UserFileSourceIdPathParam: str = Path( +UserFileSourceIdPathParam: UUID4 = Path( ..., title="User File Source UUID", description="The UUID index for a persisted UserFileSourceStore object." ) @@ -36,6 +39,7 @@ @router.cbv class FastAPIFileSources: file_source_instances_manager: FileSourceInstancesManager = depends(FileSourceInstancesManager) + user: User = DependsOnUser @router.get( "/api/file_source_templates", @@ -92,7 +96,7 @@ def instance_index( def instances_show( self, trans: ProvidesUserContext = DependsOnTrans, - user_file_source_id: str = UserFileSourceIdPathParam, + user_file_source_id: UUID4 = UserFileSourceIdPathParam, ) -> UserFileSourceModel: return self.file_source_instances_manager.show(trans, user_file_source_id) @@ -104,7 +108,7 @@ def instances_show( def update_instance( self, trans: ProvidesUserContext = DependsOnTrans, - user_file_source_id: str = UserFileSourceIdPathParam, + user_file_source_id: UUID4 = UserFileSourceIdPathParam, payload: ModifyInstancePayload = Body(...), ) -> UserFileSourceModel: return self.file_source_instances_manager.modify_instance(trans, user_file_source_id, payload) @@ -118,7 +122,7 @@ def update_instance( def purge_instance( self, trans: ProvidesUserContext = DependsOnTrans, - user_file_source_id: str = UserFileSourceIdPathParam, + user_file_source_id: UUID4 = UserFileSourceIdPathParam, ): self.file_source_instances_manager.purge_instance(trans, user_file_source_id) return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/lib/galaxy/webapps/galaxy/api/object_store.py b/lib/galaxy/webapps/galaxy/api/object_store.py index 886f526420fd..3927ff17b1d5 100644 --- a/lib/galaxy/webapps/galaxy/api/object_store.py +++ b/lib/galaxy/webapps/galaxy/api/object_store.py @@ -15,6 +15,7 @@ Response, status, ) +from pydantic import UUID4 from galaxy.exceptions import ( ObjectNotFound, @@ -27,6 +28,7 @@ ObjectStoreInstancesManager, UserConcreteObjectStoreModel, ) +from galaxy.model import User from galaxy.objectstore import ( BaseObjectStore, ConcreteObjectStoreModel, @@ -36,6 +38,7 @@ from . import ( depends, DependsOnTrans, + DependsOnUser, Router, ) @@ -47,7 +50,7 @@ ..., title="Concrete Object Store ID", description="The concrete object store ID." ) -UserObjectStoreIdPathParam: str = Path( +UserObjectStoreIdPathParam: UUID4 = Path( ..., title="User Object Store UUID", description="The UUID used to identify a persisted UserObjectStore object.", @@ -95,6 +98,7 @@ def index( def create( self, trans: ProvidesUserContext = DependsOnTrans, + user: User = DependsOnUser, payload: CreateInstancePayload = Body(...), ) -> UserConcreteObjectStoreModel: return self.object_store_instance_manager.create_instance(trans, payload) @@ -107,6 +111,7 @@ def create( def test_instance_configuration( self, trans: ProvidesUserContext = DependsOnTrans, + user: User = DependsOnUser, payload: CreateInstancePayload = Body(...), ) -> PluginStatus: return self.object_store_instance_manager.plugin_status(trans, payload) @@ -119,6 +124,7 @@ def test_instance_configuration( def instance_index( self, trans: ProvidesUserContext = DependsOnTrans, + user: User = DependsOnUser, ) -> List[UserConcreteObjectStoreModel]: return self.object_store_instance_manager.index(trans) @@ -130,7 +136,8 @@ def instance_index( def instances_show( self, trans: ProvidesUserContext = DependsOnTrans, - user_object_store_id: str = UserObjectStoreIdPathParam, + user: User = DependsOnUser, + user_object_store_id: UUID4 = UserObjectStoreIdPathParam, ) -> UserConcreteObjectStoreModel: return self.object_store_instance_manager.show(trans, user_object_store_id) @@ -153,7 +160,8 @@ def show_info( def update_instance( self, trans: ProvidesUserContext = DependsOnTrans, - user_object_store_id: str = UserObjectStoreIdPathParam, + user: User = DependsOnUser, + user_object_store_id: UUID4 = UserObjectStoreIdPathParam, payload: ModifyInstancePayload = Body(...), ) -> UserConcreteObjectStoreModel: return self.object_store_instance_manager.modify_instance(trans, user_object_store_id, payload) @@ -167,7 +175,8 @@ def update_instance( def purge_instance( self, trans: ProvidesUserContext = DependsOnTrans, - user_object_store_id: str = UserObjectStoreIdPathParam, + user: User = DependsOnUser, + user_object_store_id: UUID4 = UserObjectStoreIdPathParam, ): self.object_store_instance_manager.purge_instance(trans, user_object_store_id) return Response(status_code=status.HTTP_204_NO_CONTENT) @@ -181,6 +190,7 @@ def purge_instance( def index_templates( self, trans: ProvidesUserContext = DependsOnTrans, + user: User = DependsOnUser, ) -> ObjectStoreTemplateSummaries: return self.object_store_instance_manager.summaries