fractal-server 2.6.3__py3-none-any.whl → 2.7.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/app/models/v2/__init__.py +2 -0
  3. fractal_server/app/models/v2/task.py +27 -0
  4. fractal_server/app/routes/api/v2/__init__.py +4 -0
  5. fractal_server/app/routes/api/v2/_aux_functions.py +0 -61
  6. fractal_server/app/routes/api/v2/_aux_functions_tasks.py +209 -0
  7. fractal_server/app/routes/api/v2/submit.py +16 -3
  8. fractal_server/app/routes/api/v2/task.py +59 -72
  9. fractal_server/app/routes/api/v2/task_collection.py +20 -4
  10. fractal_server/app/routes/api/v2/task_collection_custom.py +44 -18
  11. fractal_server/app/routes/api/v2/task_group.py +130 -0
  12. fractal_server/app/routes/api/v2/workflow.py +24 -3
  13. fractal_server/app/routes/api/v2/workflowtask.py +4 -7
  14. fractal_server/app/routes/auth/_aux_auth.py +42 -0
  15. fractal_server/app/schemas/v2/__init__.py +5 -0
  16. fractal_server/app/schemas/v2/task.py +2 -1
  17. fractal_server/app/schemas/v2/task_group.py +23 -0
  18. fractal_server/app/schemas/v2/workflow.py +5 -0
  19. fractal_server/app/schemas/v2/workflowtask.py +4 -0
  20. fractal_server/migrations/env.py +4 -1
  21. fractal_server/migrations/versions/7cf1baae8fb4_task_group_v2.py +66 -0
  22. fractal_server/tasks/v2/background_operations.py +16 -35
  23. fractal_server/tasks/v2/background_operations_ssh.py +15 -2
  24. fractal_server/tasks/v2/database_operations.py +54 -0
  25. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0a0.dist-info}/METADATA +1 -1
  26. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0a0.dist-info}/RECORD +29 -24
  27. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0a0.dist-info}/LICENSE +0 -0
  28. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0a0.dist-info}/WHEEL +0 -0
  29. {fractal_server-2.6.3.dist-info → fractal_server-2.7.0a0.dist-info}/entry_points.txt +0 -0
@@ -1 +1 @@
1
- __VERSION__ = "2.6.3"
1
+ __VERSION__ = "2.7.0a0"
@@ -6,6 +6,7 @@ from .collection_state import CollectionStateV2
6
6
  from .dataset import DatasetV2
7
7
  from .job import JobV2
8
8
  from .project import ProjectV2
9
+ from .task import TaskGroupV2
9
10
  from .task import TaskV2
10
11
  from .workflow import WorkflowV2
11
12
  from .workflowtask import WorkflowTaskV2
@@ -16,6 +17,7 @@ __all__ = [
16
17
  "JobV2",
17
18
  "ProjectV2",
18
19
  "CollectionStateV2",
20
+ "TaskGroupV2",
19
21
  "TaskV2",
20
22
  "WorkflowTaskV2",
21
23
  "WorkflowV2",
@@ -1,12 +1,17 @@
1
+ from datetime import datetime
1
2
  from typing import Any
2
3
  from typing import Optional
3
4
 
4
5
  from pydantic import HttpUrl
5
6
  from sqlalchemy import Column
7
+ from sqlalchemy.types import DateTime
6
8
  from sqlalchemy.types import JSON
7
9
  from sqlmodel import Field
10
+ from sqlmodel import Relationship
8
11
  from sqlmodel import SQLModel
9
12
 
13
+ from fractal_server.utils import get_timestamp
14
+
10
15
 
11
16
  class TaskV2(SQLModel, table=True):
12
17
 
@@ -39,3 +44,25 @@ class TaskV2(SQLModel, table=True):
39
44
 
40
45
  input_types: dict[str, bool] = Field(sa_column=Column(JSON), default={})
41
46
  output_types: dict[str, bool] = Field(sa_column=Column(JSON), default={})
47
+
48
+ taskgroupv2_id: Optional[int] = Field(foreign_key="taskgroupv2.id")
49
+
50
+
51
+ class TaskGroupV2(SQLModel, table=True):
52
+
53
+ id: Optional[int] = Field(default=None, primary_key=True)
54
+
55
+ user_id: int = Field(foreign_key="user_oauth.id")
56
+ user_group_id: Optional[int] = Field(foreign_key="usergroup.id")
57
+
58
+ active: bool = True
59
+ task_list: list[TaskV2] = Relationship(
60
+ sa_relationship_kwargs=dict(
61
+ lazy="selectin", cascade="all, delete-orphan"
62
+ ),
63
+ )
64
+
65
+ timestamp_created: datetime = Field(
66
+ default_factory=get_timestamp,
67
+ sa_column=Column(DateTime(timezone=True), nullable=False),
68
+ )
@@ -12,6 +12,7 @@ from .submit import router as submit_job_router_v2
12
12
  from .task import router as task_router_v2
13
13
  from .task_collection import router as task_collection_router_v2
14
14
  from .task_collection_custom import router as task_collection_router_v2_custom
15
+ from .task_group import router as task_group_router_v2
15
16
  from .workflow import router as workflow_router_v2
16
17
  from .workflowtask import router as workflowtask_router_v2
17
18
  from fractal_server.config import get_settings
@@ -37,6 +38,9 @@ router_api_v2.include_router(
37
38
  tags=["V2 Task Collection"],
38
39
  )
39
40
  router_api_v2.include_router(task_router_v2, prefix="/task", tags=["V2 Task"])
41
+ router_api_v2.include_router(
42
+ task_group_router_v2, prefix="/task-group", tags=["V2 TaskGroup"]
43
+ )
40
44
  router_api_v2.include_router(workflow_router_v2, tags=["V2 Workflow"])
41
45
  router_api_v2.include_router(workflowtask_router_v2, tags=["V2 WorkflowTask"])
42
46
  router_api_v2.include_router(status_router_v2, tags=["V2 Status"])
@@ -21,8 +21,6 @@ from ....models.v2 import TaskV2
21
21
  from ....models.v2 import WorkflowTaskV2
22
22
  from ....models.v2 import WorkflowV2
23
23
  from ....schemas.v2 import JobStatusTypeV2
24
- from ...aux.validate_user_settings import verify_user_has_settings
25
- from fractal_server.app.models import UserOAuth
26
24
  from fractal_server.images import Filters
27
25
 
28
26
 
@@ -320,65 +318,6 @@ async def _get_job_check_owner(
320
318
  return dict(job=job, project=project)
321
319
 
322
320
 
323
- async def _get_task_check_owner(
324
- *,
325
- task_id: int,
326
- user: UserOAuth,
327
- db: AsyncSession,
328
- ) -> TaskV2:
329
- """
330
- Get a task, after access control.
331
-
332
- This check constitutes a preliminary version of access control:
333
- if the current user is not a superuser and differs from the task owner
334
- (including when `owner is None`), we raise an 403 HTTP Exception.
335
-
336
- Args:
337
- task_id:
338
- user:
339
- db:
340
-
341
- Returns:
342
- The task object.
343
-
344
- Raises:
345
- HTTPException(status_code=404_NOT_FOUND):
346
- If the task does not exist
347
- HTTPException(status_code=403_FORBIDDEN):
348
- If the user does not have rights to edit this task.
349
- """
350
- task = await db.get(TaskV2, task_id)
351
- if not task:
352
- raise HTTPException(
353
- status_code=status.HTTP_404_NOT_FOUND,
354
- detail=f"TaskV2 {task_id} not found.",
355
- )
356
-
357
- if not user.is_superuser:
358
- if task.owner is None:
359
- raise HTTPException(
360
- status_code=status.HTTP_403_FORBIDDEN,
361
- detail=(
362
- "Only a superuser can modify a TaskV2 with `owner=None`."
363
- ),
364
- )
365
- else:
366
- if user.username:
367
- owner = user.username
368
- else:
369
- verify_user_has_settings(user)
370
- owner = user.settings.slurm_user
371
- if owner != task.owner:
372
- raise HTTPException(
373
- status_code=status.HTTP_403_FORBIDDEN,
374
- detail=(
375
- f"Current user ({owner}) cannot modify TaskV2 "
376
- f"{task.id} with different owner ({task.owner})."
377
- ),
378
- )
379
- return task
380
-
381
-
382
321
  def _get_submitted_jobs_statement() -> SelectOfScalar:
383
322
  """
384
323
  Returns:
@@ -0,0 +1,209 @@
1
+ """
2
+ Auxiliary functions to get task and task-group object from the database or
3
+ perform simple checks
4
+ """
5
+ from typing import Optional
6
+
7
+ from fastapi import HTTPException
8
+ from fastapi import status
9
+ from sqlmodel import select
10
+
11
+ from ....db import AsyncSession
12
+ from ....models import LinkUserGroup
13
+ from ....models.v2 import TaskGroupV2
14
+ from ....models.v2 import TaskV2
15
+ from ...auth._aux_auth import _get_default_user_group_id
16
+ from ...auth._aux_auth import _verify_user_belongs_to_group
17
+
18
+
19
+ async def _get_task_group_or_404(
20
+ *, task_group_id: int, db: AsyncSession
21
+ ) -> TaskGroupV2:
22
+ """
23
+ Get an existing task group or raise a 404.
24
+
25
+ Arguments:
26
+ task_group_id: The TaskGroupV2 id
27
+ db: An asynchronous db session
28
+ """
29
+ task_group = await db.get(TaskGroupV2, task_group_id)
30
+ if task_group is None:
31
+ raise HTTPException(
32
+ status_code=status.HTTP_404_NOT_FOUND,
33
+ detail=f"TaskGroupV2 {task_group_id} not found",
34
+ )
35
+ return task_group
36
+
37
+
38
+ async def _get_task_group_read_access(
39
+ *,
40
+ task_group_id: int,
41
+ user_id: int,
42
+ db: AsyncSession,
43
+ ) -> TaskGroupV2:
44
+ """
45
+ Get a task group or raise a 403 if user has no read access.
46
+
47
+ Arguments:
48
+ task_group_id: ID of the required task group.
49
+ user_id: ID of the current user.
50
+ db: An asynchronous db session.
51
+ """
52
+ task_group = await _get_task_group_or_404(
53
+ task_group_id=task_group_id, db=db
54
+ )
55
+
56
+ # Prepare exception to be used below
57
+ forbidden_exception = HTTPException(
58
+ status_code=status.HTTP_403_FORBIDDEN,
59
+ detail=(
60
+ "Current user has no read access to TaskGroupV2 "
61
+ f"{task_group_id}.",
62
+ ),
63
+ )
64
+
65
+ if task_group.user_id == user_id:
66
+ return task_group
67
+ elif task_group.user_group_id is None:
68
+ raise forbidden_exception
69
+ else:
70
+ stm = (
71
+ select(LinkUserGroup)
72
+ .where(LinkUserGroup.group_id == task_group.user_group_id)
73
+ .where(LinkUserGroup.user_id == user_id)
74
+ )
75
+ res = await db.execute(stm)
76
+ link = res.scalar_one_or_none()
77
+ if link is None:
78
+ raise forbidden_exception
79
+ else:
80
+ return task_group
81
+
82
+
83
+ async def _get_task_group_full_access(
84
+ *,
85
+ task_group_id: int,
86
+ user_id: int,
87
+ db: AsyncSession,
88
+ ) -> TaskGroupV2:
89
+ """
90
+ Get a task group or raise a 403 if user has no full access.
91
+
92
+ Arguments:
93
+ task_group_id: ID of the required task group.
94
+ user_id: ID of the current user.
95
+ db: An asynchronous db session
96
+ """
97
+ task_group = await _get_task_group_or_404(
98
+ task_group_id=task_group_id, db=db
99
+ )
100
+
101
+ if task_group.user_id == user_id:
102
+ return task_group
103
+ else:
104
+ raise HTTPException(
105
+ status_code=status.HTTP_403_FORBIDDEN,
106
+ detail=(
107
+ "Current user has no full access to "
108
+ f"TaskGroupV2 {task_group_id}.",
109
+ ),
110
+ )
111
+
112
+
113
+ async def _get_task_or_404(*, task_id: int, db: AsyncSession) -> TaskV2:
114
+ """
115
+ Get an existing task or raise a 404.
116
+
117
+ Arguments:
118
+ task_id: ID of the required task.
119
+ db: An asynchronous db session
120
+ """
121
+ task = await db.get(TaskV2, task_id)
122
+ if task is None:
123
+ raise HTTPException(
124
+ status_code=status.HTTP_404_NOT_FOUND,
125
+ detail=f"TaskV2 {task_id} not found",
126
+ )
127
+ return task
128
+
129
+
130
+ async def _get_task_full_access(
131
+ *,
132
+ task_id: int,
133
+ user_id: int,
134
+ db: AsyncSession,
135
+ ) -> TaskV2:
136
+ """
137
+ Get an existing task or raise a 404.
138
+
139
+ Arguments:
140
+ task_id: ID of the required task.
141
+ user_id: ID of the current user.
142
+ db: An asynchronous db session.
143
+ """
144
+ task = await _get_task_or_404(task_id=task_id, db=db)
145
+ await _get_task_group_full_access(
146
+ task_group_id=task.taskgroupv2_id, user_id=user_id, db=db
147
+ )
148
+ return task
149
+
150
+
151
+ async def _get_task_read_access(
152
+ *,
153
+ task_id: int,
154
+ user_id: int,
155
+ db: AsyncSession,
156
+ require_active: bool = False,
157
+ ) -> TaskV2:
158
+ """
159
+ Get an existing task or raise a 404.
160
+
161
+ Arguments:
162
+ task_id: ID of the required task.
163
+ user_id: ID of the current user.
164
+ db: An asynchronous db session.
165
+ require_active: If set, fail when the task group is not `active`
166
+ """
167
+ task = await _get_task_or_404(task_id=task_id, db=db)
168
+ task_group = await _get_task_group_read_access(
169
+ task_group_id=task.taskgroupv2_id, user_id=user_id, db=db
170
+ )
171
+ if require_active:
172
+ if not task_group.active:
173
+ raise HTTPException(
174
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
175
+ detail="Cannot insert non-active tasks into a workflow.",
176
+ )
177
+ return task
178
+
179
+
180
+ async def _get_valid_user_group_id(
181
+ *,
182
+ user_group_id: Optional[int] = None,
183
+ private: bool,
184
+ user_id: int,
185
+ db: AsyncSession,
186
+ ) -> Optional[int]:
187
+ """
188
+ Validate query parameters for endpoints that create some task(s).
189
+
190
+ Arguments:
191
+ user_group_id:
192
+ private:
193
+ user_id: ID of the current user
194
+ db: An asynchronous db session.
195
+ """
196
+ if (user_group_id is not None) and (private is True):
197
+ raise HTTPException(
198
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
199
+ detail=f"Cannot set both {user_group_id=} and {private=}",
200
+ )
201
+ elif private is True:
202
+ user_group_id = None
203
+ elif user_group_id is None:
204
+ user_group_id = await _get_default_user_group_id(db=db)
205
+ else:
206
+ await _verify_user_belongs_to_group(
207
+ user_id=user_id, user_group_id=user_group_id, db=db
208
+ )
209
+ return user_group_id
@@ -32,6 +32,9 @@ from ._aux_functions import _get_dataset_check_owner
32
32
  from ._aux_functions import _get_workflow_check_owner
33
33
  from ._aux_functions import clean_app_job_list_v2
34
34
  from fractal_server.app.models import UserOAuth
35
+ from fractal_server.app.routes.api.v2._aux_functions_tasks import (
36
+ _get_task_read_access,
37
+ )
35
38
  from fractal_server.app.routes.auth import current_active_verified_user
36
39
 
37
40
 
@@ -83,15 +86,14 @@ async def apply_workflow(
83
86
  workflow = await _get_workflow_check_owner(
84
87
  project_id=project_id, workflow_id=workflow_id, user_id=user.id, db=db
85
88
  )
86
-
87
- if not workflow.task_list:
89
+ num_tasks = len(workflow.task_list)
90
+ if num_tasks == 0:
88
91
  raise HTTPException(
89
92
  status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
90
93
  detail=f"Workflow {workflow_id} has empty task list",
91
94
  )
92
95
 
93
96
  # Set values of first_task_index and last_task_index
94
- num_tasks = len(workflow.task_list)
95
97
  try:
96
98
  first_task_index, last_task_index = set_start_and_last_task_index(
97
99
  num_tasks,
@@ -110,6 +112,17 @@ async def apply_workflow(
110
112
  ),
111
113
  )
112
114
 
115
+ # Check that tasks have read-access and are `active`
116
+ for wftask in workflow.task_list[
117
+ first_task_index : last_task_index + 1 # noqa: E203
118
+ ]:
119
+ await _get_task_read_access(
120
+ user_id=user.id,
121
+ task_id=wftask.task_id,
122
+ require_active=True,
123
+ db=db,
124
+ )
125
+
113
126
  # Validate user settings
114
127
  FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
115
128
  user_settings = await validate_user_settings(
@@ -6,23 +6,26 @@ from fastapi import Depends
6
6
  from fastapi import HTTPException
7
7
  from fastapi import Response
8
8
  from fastapi import status
9
+ from sqlmodel import or_
9
10
  from sqlmodel import select
10
11
 
11
- from .....logger import set_logger
12
- from ....db import AsyncSession
13
- from ....db import get_async_db
14
- from ....models.v1 import Task as TaskV1
15
- from ....models.v2 import TaskV2
16
- from ....models.v2 import WorkflowTaskV2
17
- from ....models.v2 import WorkflowV2
18
- from ....schemas.v2 import TaskCreateV2
19
- from ....schemas.v2 import TaskReadV2
20
- from ....schemas.v2 import TaskUpdateV2
21
12
  from ...aux.validate_user_settings import verify_user_has_settings
22
- from ._aux_functions import _get_task_check_owner
13
+ from ._aux_functions_tasks import _get_task_full_access
14
+ from ._aux_functions_tasks import _get_task_read_access
15
+ from ._aux_functions_tasks import _get_valid_user_group_id
16
+ from fractal_server.app.db import AsyncSession
17
+ from fractal_server.app.db import get_async_db
18
+ from fractal_server.app.models import LinkUserGroup
23
19
  from fractal_server.app.models import UserOAuth
20
+ from fractal_server.app.models.v1 import Task as TaskV1
21
+ from fractal_server.app.models.v2 import TaskGroupV2
22
+ from fractal_server.app.models.v2 import TaskV2
24
23
  from fractal_server.app.routes.auth import current_active_user
25
24
  from fractal_server.app.routes.auth import current_active_verified_user
25
+ from fractal_server.app.schemas.v2 import TaskCreateV2
26
+ from fractal_server.app.schemas.v2 import TaskReadV2
27
+ from fractal_server.app.schemas.v2 import TaskUpdateV2
28
+ from fractal_server.logger import set_logger
26
29
 
27
30
  router = APIRouter()
28
31
 
@@ -39,7 +42,21 @@ async def get_list_task(
39
42
  """
40
43
  Get list of available tasks
41
44
  """
42
- stm = select(TaskV2)
45
+ stm = (
46
+ select(TaskV2)
47
+ .join(TaskGroupV2)
48
+ .where(TaskGroupV2.id == TaskV2.taskgroupv2_id)
49
+ .where(
50
+ or_(
51
+ TaskGroupV2.user_id == user.id,
52
+ TaskGroupV2.user_group_id.in_(
53
+ select(LinkUserGroup.group_id).where(
54
+ LinkUserGroup.user_id == user.id
55
+ )
56
+ ),
57
+ )
58
+ )
59
+ )
43
60
  res = await db.execute(stm)
44
61
  task_list = res.scalars().all()
45
62
  await db.close()
@@ -62,12 +79,7 @@ async def get_task(
62
79
  """
63
80
  Get info on a specific task
64
81
  """
65
- task = await db.get(TaskV2, task_id)
66
- await db.close()
67
- if not task:
68
- raise HTTPException(
69
- status_code=status.HTTP_404_NOT_FOUND, detail="TaskV2 not found"
70
- )
82
+ task = await _get_task_read_access(task_id=task_id, user_id=user.id, db=db)
71
83
  return task
72
84
 
73
85
 
@@ -83,7 +95,9 @@ async def patch_task(
83
95
  """
84
96
 
85
97
  # Retrieve task from database
86
- db_task = await _get_task_check_owner(task_id=task_id, user=user, db=db)
98
+ db_task = await _get_task_full_access(
99
+ task_id=task_id, user_id=user.id, db=db
100
+ )
87
101
  update = task_update.dict(exclude_unset=True)
88
102
 
89
103
  # Forbid changes that set a previously unset command
@@ -112,6 +126,8 @@ async def patch_task(
112
126
  )
113
127
  async def create_task(
114
128
  task: TaskCreateV2,
129
+ user_group_id: Optional[int] = None,
130
+ private: bool = False,
115
131
  user: UserOAuth = Depends(current_active_verified_user),
116
132
  db: AsyncSession = Depends(get_async_db),
117
133
  ) -> Optional[TaskReadV2]:
@@ -119,6 +135,14 @@ async def create_task(
119
135
  Create a new task
120
136
  """
121
137
 
138
+ # Validate query parameters related to user-group ownership
139
+ user_group_id = await _get_valid_user_group_id(
140
+ user_group_id=user_group_id,
141
+ private=private,
142
+ user_id=user.id,
143
+ db=db,
144
+ )
145
+
122
146
  if task.command_non_parallel is None:
123
147
  task_type = "parallel"
124
148
  elif task.command_parallel is None:
@@ -148,7 +172,7 @@ async def create_task(
148
172
  ),
149
173
  )
150
174
 
151
- # Set task.owner attribute
175
+ # Set task.owner attribute - FIXME: remove this block
152
176
  if user.username:
153
177
  owner = user.username
154
178
  else:
@@ -186,7 +210,14 @@ async def create_task(
186
210
  )
187
211
  # Add task
188
212
  db_task = TaskV2(**task.dict(), owner=owner, type=task_type)
189
- db.add(db_task)
213
+
214
+ db_task_group = TaskGroupV2(
215
+ user_id=user.id,
216
+ user_group_id=user_group_id,
217
+ active=True,
218
+ task_list=[db_task],
219
+ )
220
+ db.add(db_task_group)
190
221
  await db.commit()
191
222
  await db.refresh(db_task)
192
223
  await db.close()
@@ -202,54 +233,10 @@ async def delete_task(
202
233
  """
203
234
  Delete a task
204
235
  """
205
-
206
- db_task = await _get_task_check_owner(task_id=task_id, user=user, db=db)
207
-
208
- # Check that the TaskV2 is not in relationship with some WorkflowTaskV2
209
- stm = select(WorkflowTaskV2).filter(WorkflowTaskV2.task_id == task_id)
210
- res = await db.execute(stm)
211
- workflow_tasks = res.scalars().all()
212
-
213
- if workflow_tasks:
214
- # Find IDs of all affected workflows
215
- workflow_ids = set(wftask.workflow_id for wftask in workflow_tasks)
216
- # Fetch all affected workflows from DB
217
- stm = select(WorkflowV2).where(WorkflowV2.id.in_(workflow_ids))
218
- res = await db.execute(stm)
219
- workflows = res.scalars().all()
220
-
221
- # Find which workflows are associated to the current user
222
- workflows_current_user = [
223
- wf for wf in workflows if user in wf.project.user_list
224
- ]
225
- if workflows_current_user:
226
- current_user_msg = (
227
- "For the current-user workflows (listed below),"
228
- " you can update the task or remove the workflows.\n"
229
- )
230
- current_user_msg += "\n".join(
231
- [
232
- f"* '{wf.name}' (id={wf.id})"
233
- for wf in workflows_current_user
234
- ]
235
- )
236
- else:
237
- current_user_msg = ""
238
-
239
- # Count workflows of current users or other users
240
- num_workflows_current_user = len(workflows_current_user)
241
- num_workflows_other_users = len(workflows) - num_workflows_current_user
242
-
243
- raise HTTPException(
244
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
245
- detail=(
246
- f"Cannot remove Task with id={task_id}: it is currently in "
247
- f"use in {num_workflows_current_user} current-user workflows "
248
- f"and in {num_workflows_other_users} other-users workflows.\n"
249
- f"{current_user_msg}"
250
- ),
251
- )
252
-
253
- await db.delete(db_task)
254
- await db.commit()
255
- return Response(status_code=status.HTTP_204_NO_CONTENT)
236
+ raise HTTPException(
237
+ status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
238
+ detail=(
239
+ "Cannot delete single tasks, "
240
+ "please operate directly on task groups."
241
+ ),
242
+ )
@@ -2,6 +2,7 @@ import json
2
2
  from pathlib import Path
3
3
  from shutil import copy as shell_copy
4
4
  from tempfile import TemporaryDirectory
5
+ from typing import Optional
5
6
 
6
7
  from fastapi import APIRouter
7
8
  from fastapi import BackgroundTasks
@@ -26,6 +27,7 @@ from ....schemas.v2 import CollectionStatusV2
26
27
  from ....schemas.v2 import TaskCollectPipV2
27
28
  from ....schemas.v2 import TaskReadV2
28
29
  from ...aux.validate_user_settings import validate_user_settings
30
+ from ._aux_functions_tasks import _get_valid_user_group_id
29
31
  from fractal_server.app.models import UserOAuth
30
32
  from fractal_server.app.routes.auth import current_active_user
31
33
  from fractal_server.app.routes.auth import current_active_verified_user
@@ -69,6 +71,8 @@ async def collect_tasks_pip(
69
71
  background_tasks: BackgroundTasks,
70
72
  response: Response,
71
73
  request: Request,
74
+ private: bool = False,
75
+ user_group_id: Optional[int] = None,
72
76
  user: UserOAuth = Depends(current_active_verified_user),
73
77
  db: AsyncSession = Depends(get_async_db),
74
78
  ) -> CollectionStateReadV2:
@@ -112,6 +116,14 @@ async def collect_tasks_pip(
112
116
  user=user, backend=settings.FRACTAL_RUNNER_BACKEND, db=db
113
117
  )
114
118
 
119
+ # Validate query parameters related to user-group ownership
120
+ user_group_id = await _get_valid_user_group_id(
121
+ user_group_id=user_group_id,
122
+ private=private,
123
+ user_id=user.id,
124
+ db=db,
125
+ )
126
+
115
127
  # END of SSH/non-SSH common part
116
128
 
117
129
  if settings.FRACTAL_RUNNER_BACKEND == "slurm_ssh":
@@ -140,10 +152,12 @@ async def collect_tasks_pip(
140
152
 
141
153
  background_tasks.add_task(
142
154
  background_collect_pip_ssh,
143
- state.id,
144
- task_pkg,
145
- fractal_ssh,
146
- user_settings.ssh_tasks_dir,
155
+ state_id=state.id,
156
+ task_pkg=task_pkg,
157
+ fractal_ssh=fractal_ssh,
158
+ tasks_base_dir=user_settings.ssh_tasks_dir,
159
+ user_id=user.id,
160
+ user_group_id=user_group_id,
147
161
  )
148
162
 
149
163
  response.status_code = status.HTTP_201_CREATED
@@ -284,6 +298,8 @@ async def collect_tasks_pip(
284
298
  state_id=state.id,
285
299
  venv_path=venv_path,
286
300
  task_pkg=task_pkg,
301
+ user_id=user.id,
302
+ user_group_id=user_group_id,
287
303
  )
288
304
  logger.debug(
289
305
  "Task-collection endpoint: start background collection "