fractal-server 2.17.0a4__py3-none-any.whl → 2.17.0a6__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 (67) hide show
  1. fractal_server/__init__.py +1 -1
  2. fractal_server/__main__.py +22 -26
  3. fractal_server/app/models/security.py +19 -21
  4. fractal_server/app/models/user_settings.py +1 -0
  5. fractal_server/app/models/v2/task_group.py +1 -0
  6. fractal_server/app/routes/admin/v2/accounting.py +3 -3
  7. fractal_server/app/routes/admin/v2/impersonate.py +2 -2
  8. fractal_server/app/routes/admin/v2/job.py +6 -6
  9. fractal_server/app/routes/admin/v2/profile.py +18 -4
  10. fractal_server/app/routes/admin/v2/project.py +2 -2
  11. fractal_server/app/routes/admin/v2/resource.py +8 -8
  12. fractal_server/app/routes/admin/v2/task.py +11 -2
  13. fractal_server/app/routes/admin/v2/task_group.py +16 -12
  14. fractal_server/app/routes/admin/v2/task_group_lifecycle.py +4 -4
  15. fractal_server/app/routes/api/__init__.py +14 -5
  16. fractal_server/app/routes/api/v2/dataset.py +10 -19
  17. fractal_server/app/routes/api/v2/history.py +8 -8
  18. fractal_server/app/routes/api/v2/images.py +5 -5
  19. fractal_server/app/routes/api/v2/job.py +8 -8
  20. fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
  21. fractal_server/app/routes/api/v2/project.py +6 -6
  22. fractal_server/app/routes/api/v2/status_legacy.py +2 -2
  23. fractal_server/app/routes/api/v2/submit.py +25 -29
  24. fractal_server/app/routes/api/v2/task.py +6 -7
  25. fractal_server/app/routes/api/v2/task_collection.py +4 -3
  26. fractal_server/app/routes/api/v2/task_collection_custom.py +4 -3
  27. fractal_server/app/routes/api/v2/task_collection_pixi.py +2 -2
  28. fractal_server/app/routes/api/v2/task_group.py +6 -6
  29. fractal_server/app/routes/api/v2/task_group_lifecycle.py +4 -4
  30. fractal_server/app/routes/api/v2/task_version_update.py +3 -3
  31. fractal_server/app/routes/api/v2/workflow.py +9 -9
  32. fractal_server/app/routes/api/v2/workflow_import.py +2 -2
  33. fractal_server/app/routes/api/v2/workflowtask.py +5 -5
  34. fractal_server/app/routes/auth/__init__.py +34 -5
  35. fractal_server/app/routes/auth/current_user.py +22 -72
  36. fractal_server/app/routes/auth/group.py +8 -35
  37. fractal_server/app/routes/auth/oauth.py +1 -1
  38. fractal_server/app/routes/auth/register.py +2 -2
  39. fractal_server/app/routes/auth/users.py +6 -48
  40. fractal_server/app/schemas/__init__.py +0 -1
  41. fractal_server/app/schemas/user.py +23 -0
  42. fractal_server/app/schemas/v2/__init__.py +1 -0
  43. fractal_server/app/schemas/v2/task_group.py +5 -0
  44. fractal_server/app/security/__init__.py +134 -46
  45. fractal_server/app/security/signup_email.py +52 -34
  46. fractal_server/config/__init__.py +6 -0
  47. fractal_server/config/_data.py +68 -0
  48. fractal_server/config/_email.py +10 -47
  49. fractal_server/config/_main.py +3 -56
  50. fractal_server/config/_oauth.py +2 -2
  51. fractal_server/main.py +3 -2
  52. fractal_server/migrations/versions/f65ee53991e3_user_settings_related.py +67 -0
  53. fractal_server/runner/executors/slurm_common/base_slurm_runner.py +1 -1
  54. fractal_server/runner/executors/slurm_common/slurm_config.py +5 -8
  55. fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
  56. fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
  57. fractal_server/runner/v2/_slurm_ssh.py +2 -1
  58. fractal_server/runner/v2/_slurm_sudo.py +1 -1
  59. fractal_server/runner/v2/submit_workflow.py +12 -12
  60. {fractal_server-2.17.0a4.dist-info → fractal_server-2.17.0a6.dist-info}/METADATA +4 -6
  61. {fractal_server-2.17.0a4.dist-info → fractal_server-2.17.0a6.dist-info}/RECORD +64 -65
  62. fractal_server/app/routes/aux/validate_user_settings.py +0 -76
  63. fractal_server/app/schemas/user_settings.py +0 -63
  64. fractal_server/app/user_settings.py +0 -32
  65. {fractal_server-2.17.0a4.dist-info → fractal_server-2.17.0a6.dist-info}/WHEEL +0 -0
  66. {fractal_server-2.17.0a4.dist-info → fractal_server-2.17.0a6.dist-info}/entry_points.txt +0 -0
  67. {fractal_server-2.17.0a4.dist-info → fractal_server-2.17.0a6.dist-info}/licenses/LICENSE +0 -0
@@ -6,28 +6,23 @@ from fastapi import Depends
6
6
  from fastapi import HTTPException
7
7
  from fastapi import status
8
8
  from fastapi_users import exceptions
9
- from fastapi_users import schemas
10
9
  from fastapi_users.router.common import ErrorCode
11
10
  from sqlalchemy.ext.asyncio import AsyncSession
12
11
  from sqlmodel import func
13
12
  from sqlmodel import select
14
13
 
15
- from . import current_active_superuser
14
+ from . import current_superuser_act
16
15
  from ...db import get_async_db
17
16
  from ...schemas.user import UserRead
18
17
  from ...schemas.user import UserUpdate
19
- from ..aux.validate_user_settings import verify_user_has_settings
20
18
  from ._aux_auth import _get_default_usergroup_id
21
19
  from ._aux_auth import _get_single_user_with_groups
22
20
  from ._aux_auth import FRACTAL_DEFAULT_GROUP_NAME
23
21
  from fractal_server.app.models import LinkUserGroup
24
22
  from fractal_server.app.models import UserGroup
25
23
  from fractal_server.app.models import UserOAuth
26
- from fractal_server.app.models import UserSettings
27
24
  from fractal_server.app.models.v2 import Profile
28
25
  from fractal_server.app.routes.auth._aux_auth import _user_or_404
29
- from fractal_server.app.schemas import UserSettingsRead
30
- from fractal_server.app.schemas import UserSettingsUpdate
31
26
  from fractal_server.app.schemas.user import UserUpdateGroups
32
27
  from fractal_server.app.security import get_user_manager
33
28
  from fractal_server.app.security import UserManager
@@ -43,7 +38,7 @@ logger = set_logger(__name__)
43
38
  async def get_user(
44
39
  user_id: int,
45
40
  group_ids_names: bool = True,
46
- superuser: UserOAuth = Depends(current_active_superuser),
41
+ superuser: UserOAuth = Depends(current_superuser_act),
47
42
  db: AsyncSession = Depends(get_async_db),
48
43
  ) -> UserRead:
49
44
  user = await _user_or_404(user_id, db)
@@ -57,7 +52,7 @@ async def get_user(
57
52
  async def patch_user(
58
53
  user_id: int,
59
54
  user_update: UserUpdate,
60
- current_superuser: UserOAuth = Depends(current_active_superuser),
55
+ current_superuser: UserOAuth = Depends(current_superuser_act),
61
56
  user_manager: UserManager = Depends(get_user_manager),
62
57
  db: AsyncSession = Depends(get_async_db),
63
58
  ):
@@ -84,7 +79,7 @@ async def patch_user(
84
79
  safe=False,
85
80
  request=None,
86
81
  )
87
- validated_user = schemas.model_validate(UserOAuth, user.model_dump())
82
+ validated_user = UserOAuth.model_validate(user.model_dump())
88
83
  patched_user = await db.get(
89
84
  UserOAuth, validated_user.id, populate_existing=True
90
85
  )
@@ -113,7 +108,7 @@ async def patch_user(
113
108
  @router_users.get("/users/", response_model=list[UserRead])
114
109
  async def list_users(
115
110
  profile_id: int | None = None,
116
- user: UserOAuth = Depends(current_active_superuser),
111
+ user: UserOAuth = Depends(current_superuser_act),
117
112
  db: AsyncSession = Depends(get_async_db),
118
113
  ):
119
114
  """
@@ -148,7 +143,7 @@ async def list_users(
148
143
  async def set_user_groups(
149
144
  user_id: int,
150
145
  user_update: UserUpdateGroups,
151
- superuser: UserOAuth = Depends(current_active_superuser),
146
+ superuser: UserOAuth = Depends(current_superuser_act),
152
147
  db: AsyncSession = Depends(get_async_db),
153
148
  ) -> UserRead:
154
149
  # Preliminary check that all objects exist in the db
@@ -210,40 +205,3 @@ async def set_user_groups(
210
205
  user_with_groups = await _get_single_user_with_groups(user, db)
211
206
 
212
207
  return user_with_groups
213
-
214
-
215
- @router_users.get(
216
- "/users/{user_id}/settings/", response_model=UserSettingsRead
217
- )
218
- async def get_user_settings(
219
- user_id: int,
220
- superuser: UserOAuth = Depends(current_active_superuser),
221
- db: AsyncSession = Depends(get_async_db),
222
- ) -> UserSettingsRead:
223
- user = await _user_or_404(user_id=user_id, db=db)
224
- verify_user_has_settings(user)
225
- user_settings = await db.get(UserSettings, user.user_settings_id)
226
- return user_settings
227
-
228
-
229
- @router_users.patch(
230
- "/users/{user_id}/settings/", response_model=UserSettingsRead
231
- )
232
- async def patch_user_settings(
233
- user_id: int,
234
- settings_update: UserSettingsUpdate,
235
- superuser: UserOAuth = Depends(current_active_superuser),
236
- db: AsyncSession = Depends(get_async_db),
237
- ) -> UserSettingsRead:
238
- user = await _user_or_404(user_id=user_id, db=db)
239
- verify_user_has_settings(user)
240
- user_settings = await db.get(UserSettings, user.user_settings_id)
241
-
242
- for k, v in settings_update.model_dump(exclude_unset=True).items():
243
- setattr(user_settings, k, v)
244
-
245
- db.add(user_settings)
246
- await db.commit()
247
- await db.refresh(user_settings)
248
-
249
- return user_settings
@@ -1,3 +1,2 @@
1
1
  from .user import * # noqa: F401, F403
2
2
  from .user_group import * # noqa: F401, F403
3
- from .user_settings import * # noqa: F401, F403
@@ -1,9 +1,15 @@
1
+ from typing import Annotated
2
+
1
3
  from fastapi_users import schemas
4
+ from pydantic import AfterValidator
2
5
  from pydantic import BaseModel
3
6
  from pydantic import ConfigDict
4
7
  from pydantic import EmailStr
5
8
  from pydantic import Field
6
9
 
10
+ from fractal_server.string_tools import validate_cmd
11
+ from fractal_server.types import AbsolutePathStr
12
+ from fractal_server.types import ListUniqueNonEmptyString
7
13
  from fractal_server.types import ListUniqueNonNegativeInt
8
14
  from fractal_server.types import NonEmptyStr
9
15
 
@@ -37,6 +43,13 @@ class UserRead(schemas.BaseUser[int]):
37
43
  group_ids_names: list[tuple[int, str]] | None = None
38
44
  oauth_accounts: list[OAuthAccountRead]
39
45
  profile_id: int | None = None
46
+ project_dir: str
47
+ slurm_accounts: list[str]
48
+
49
+
50
+ def _validate_cmd(value: str) -> str:
51
+ validate_cmd(value)
52
+ return value
40
53
 
41
54
 
42
55
  class UserUpdate(schemas.BaseUserUpdate):
@@ -50,6 +63,8 @@ class UserUpdate(schemas.BaseUserUpdate):
50
63
  is_superuser:
51
64
  is_verified:
52
65
  profile_id:
66
+ project_dir:
67
+ slurm_accounts:
53
68
  """
54
69
 
55
70
  model_config = ConfigDict(extra="forbid")
@@ -59,6 +74,10 @@ class UserUpdate(schemas.BaseUserUpdate):
59
74
  is_superuser: bool = None
60
75
  is_verified: bool = None
61
76
  profile_id: int | None = None
77
+ project_dir: Annotated[
78
+ AbsolutePathStr, AfterValidator(_validate_cmd)
79
+ ] = None
80
+ slurm_accounts: ListUniqueNonEmptyString = None
62
81
 
63
82
 
64
83
  class UserUpdateStrict(BaseModel):
@@ -66,9 +85,11 @@ class UserUpdateStrict(BaseModel):
66
85
  Schema for `User` self-editing.
67
86
 
68
87
  Attributes:
88
+ slurm_accounts:
69
89
  """
70
90
 
71
91
  model_config = ConfigDict(extra="forbid")
92
+ slurm_accounts: ListUniqueNonEmptyString = None
72
93
 
73
94
 
74
95
  class UserCreate(schemas.BaseUserCreate):
@@ -80,6 +101,8 @@ class UserCreate(schemas.BaseUserCreate):
80
101
  """
81
102
 
82
103
  profile_id: int | None = None
104
+ project_dir: Annotated[AbsolutePathStr, AfterValidator(_validate_cmd)]
105
+ slurm_accounts: list[str] = Field(default_factory=list)
83
106
 
84
107
 
85
108
  class UserUpdateGroups(BaseModel):
@@ -52,6 +52,7 @@ from .task_group import TaskGroupActivityStatusV2 # noqa F401
52
52
  from .task_group import TaskGroupActivityV2Read # noqa F401
53
53
  from .task_group import TaskGroupCreateV2 # noqa F401
54
54
  from .task_group import TaskGroupCreateV2Strict # noqa F401
55
+ from .task_group import TaskGroupReadSuperuser # noqa F401
55
56
  from .task_group import TaskGroupReadV2 # noqa F401
56
57
  from .task_group import TaskGroupUpdateV2 # noqa F401
57
58
  from .task_group import TaskGroupV2OriginEnum # noqa F401
@@ -37,6 +37,7 @@ class TaskGroupActivityActionV2(StrEnum):
37
37
  class TaskGroupCreateV2(BaseModel):
38
38
  model_config = ConfigDict(extra="forbid")
39
39
  user_id: int
40
+ resource_id: int
40
41
  user_group_id: int | None = None
41
42
  active: bool = True
42
43
  origin: TaskGroupV2OriginEnum
@@ -95,6 +96,10 @@ class TaskGroupReadV2(BaseModel):
95
96
  return v.isoformat()
96
97
 
97
98
 
99
+ class TaskGroupReadSuperuser(TaskGroupReadV2):
100
+ resource_id: int
101
+
102
+
98
103
  class TaskGroupUpdateV2(BaseModel):
99
104
  model_config = ConfigDict(extra="forbid")
100
105
  user_group_id: int | None = None
@@ -1,14 +1,3 @@
1
- # Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
2
- # University of Zurich
3
- #
4
- # Original authors:
5
- # Jacopo Nespolo <jacopo.nespolo@exact-lab.it>
6
- # Tommaso Comparin <tommaso.comparin@exact-lab.it>
7
- #
8
- # This file is part of Fractal and was originally developed by eXact lab S.r.l.
9
- # <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
10
- # Institute for Biomedical Research and Pelkmans Lab from the University of
11
- # Zurich.
12
1
  """
13
2
  Auth subsystem
14
3
 
@@ -30,6 +19,7 @@ import contextlib
30
19
  from collections.abc import AsyncGenerator
31
20
  from typing import Any
32
21
  from typing import Generic
22
+ from typing import Self
33
23
 
34
24
  from fastapi import Depends
35
25
  from fastapi import Request
@@ -55,10 +45,12 @@ from fractal_server.app.models import LinkUserGroup
55
45
  from fractal_server.app.models import OAuthAccount
56
46
  from fractal_server.app.models import UserGroup
57
47
  from fractal_server.app.models import UserOAuth
58
- from fractal_server.app.models import UserSettings
59
48
  from fractal_server.app.schemas.user import UserCreate
60
- from fractal_server.app.security.signup_email import mail_new_oauth_signup
49
+ from fractal_server.app.security.signup_email import (
50
+ send_fractal_email_or_log_failure,
51
+ )
61
52
  from fractal_server.config import get_email_settings
53
+ from fractal_server.config import get_settings
62
54
  from fractal_server.logger import set_logger
63
55
  from fractal_server.syringe import Inject
64
56
 
@@ -209,6 +201,131 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
209
201
  f"The password is too long (maximum length: {min_length})."
210
202
  )
211
203
 
204
+ async def oauth_callback(
205
+ self: Self,
206
+ oauth_name: str,
207
+ access_token: str,
208
+ account_id: str,
209
+ account_email: str,
210
+ expires_at: int | None = None,
211
+ refresh_token: str | None = None,
212
+ request: Request | None = None,
213
+ *,
214
+ associate_by_email: bool = False,
215
+ is_verified_by_default: bool = False,
216
+ ) -> UserOAuth:
217
+ """
218
+ Handle the callback after a successful OAuth authentication.
219
+
220
+ This method extends the corresponding `BaseUserManager` method of
221
+ > fastapi-users v14.0.1, Copyright (c) 2019 François Voron, MIT License
222
+
223
+ If the user already exists with this OAuth account, the token is
224
+ updated.
225
+
226
+ If a user with the same e-mail already exists and `associate_by_email`
227
+ is True, the OAuth account is associated to this user.
228
+ Otherwise, the `UserNotExists` exception is raised.
229
+
230
+ If the user does not exist, send an email to the Fractal admins (if
231
+ configured) and respond with a 400 error status. NOTE: This is the
232
+ function branch where the `fractal-server` implementation deviates
233
+ from the original `fastapi-users` one.
234
+
235
+ :param oauth_name: Name of the OAuth client.
236
+ :param access_token: Valid access token for the service provider.
237
+ :param account_id: models.ID of the user on the service provider.
238
+ :param account_email: E-mail of the user on the service provider.
239
+ :param expires_at: Optional timestamp at which the access token
240
+ expires.
241
+ :param refresh_token: Optional refresh token to get a
242
+ fresh access token from the service provider.
243
+ :param request: Optional FastAPI request that
244
+ triggered the operation, defaults to None
245
+ :param associate_by_email: If True, any existing user with the same
246
+ e-mail address will be associated to this user. Defaults to False.
247
+ :param is_verified_by_default: If True, the `is_verified` flag will be
248
+ set to `True` on newly created user. Make sure the OAuth Provider you
249
+ are using does verify the email address before enabling this flag.
250
+ Defaults to False.
251
+ :return: A user.
252
+ """
253
+ from fastapi import HTTPException
254
+ from fastapi import status
255
+ from fastapi_users import exceptions
256
+
257
+ oauth_account_dict = {
258
+ "oauth_name": oauth_name,
259
+ "access_token": access_token,
260
+ "account_id": account_id,
261
+ "account_email": account_email,
262
+ "expires_at": expires_at,
263
+ "refresh_token": refresh_token,
264
+ }
265
+
266
+ try:
267
+ user = await self.get_by_oauth_account(oauth_name, account_id)
268
+ except exceptions.UserNotExists:
269
+ try:
270
+ # Associate account
271
+ user = await self.get_by_email(account_email)
272
+ if not associate_by_email:
273
+ raise exceptions.UserAlreadyExists()
274
+ user = await self.user_db.add_oauth_account(
275
+ user, oauth_account_dict
276
+ )
277
+ except exceptions.UserNotExists:
278
+ # (0) Log
279
+ logger.warning(
280
+ f"Self-registration attempt by {account_email}."
281
+ )
282
+
283
+ # (1) Prepare user-facing error message
284
+ settings = Inject(get_settings)
285
+ error_msg = (
286
+ "Thank you for registering for the Fractal service. "
287
+ "Administrators have been informed to configure your "
288
+ "account and will get back to you."
289
+ )
290
+ if settings.FRACTAL_HELP_URL is not None:
291
+ error_msg = (
292
+ f"{error_msg}\n"
293
+ "You can find more information about the onboarding "
294
+ f"process at {settings.FRACTAL_HELP_URL}."
295
+ )
296
+
297
+ # (2) Send email to admins
298
+ email_settings = Inject(get_email_settings)
299
+ send_fractal_email_or_log_failure(
300
+ subject="New OAuth self-registration",
301
+ msg=(
302
+ f"User '{account_email}' tried to "
303
+ "self-register through OAuth.\n"
304
+ "Please create the Fractal account manually.\n"
305
+ "Here is the error message displayed to the "
306
+ f"user:\n{error_msg}"
307
+ ),
308
+ email_settings=email_settings.public,
309
+ )
310
+
311
+ # (3) Raise
312
+ raise HTTPException(
313
+ status_code=status.HTTP_400_BAD_REQUEST,
314
+ detail=error_msg,
315
+ )
316
+ else:
317
+ # Update oauth
318
+ for existing_oauth_account in user.oauth_accounts:
319
+ if (
320
+ existing_oauth_account.account_id == account_id
321
+ and existing_oauth_account.oauth_name == oauth_name
322
+ ):
323
+ user = await self.user_db.update_oauth_account(
324
+ user, existing_oauth_account, oauth_account_dict
325
+ )
326
+
327
+ return user
328
+
212
329
  async def on_after_register(
213
330
  self, user: UserOAuth, request: Request | None = None
214
331
  ):
@@ -216,7 +333,6 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
216
333
  f"New-user registration completed ({user.id=}, {user.email=})."
217
334
  )
218
335
  async for db in get_async_db():
219
- # Find default group
220
336
  stm = select(UserGroup).where(
221
337
  UserGroup.name == FRACTAL_DEFAULT_GROUP_NAME
222
338
  )
@@ -236,38 +352,6 @@ class UserManager(IntegerIDMixin, BaseUserManager[UserOAuth, int]):
236
352
  f"Added {user.email} user to group {default_group.id=}."
237
353
  )
238
354
 
239
- this_user = await db.get(UserOAuth, user.id)
240
-
241
- this_user.settings = UserSettings()
242
- await db.merge(this_user)
243
- await db.commit()
244
- await db.refresh(this_user)
245
- logger.info(
246
- f"Associated empty settings (id={this_user.user_settings_id}) "
247
- f"to '{this_user.email}'."
248
- )
249
-
250
- # Send mail section
251
- email_settings = Inject(get_email_settings)
252
-
253
- if this_user.oauth_accounts and email_settings.public is not None:
254
- try:
255
- logger.info(
256
- "START sending email about new signup to "
257
- f"{email_settings.public.recipients}."
258
- )
259
- mail_new_oauth_signup(
260
- msg=f"New user registered: '{this_user.email}'.",
261
- email_settings=email_settings.public,
262
- )
263
- logger.info("END sending email about new signup.")
264
- except Exception as e:
265
- logger.error(
266
- "ERROR sending notification email after oauth "
267
- f"registration of {this_user.email}. "
268
- f"Original error: '{e}'."
269
- )
270
-
271
355
 
272
356
  async def get_user_manager(
273
357
  user_db: SQLModelUserDatabaseAsync = Depends(get_user_db),
@@ -283,6 +367,8 @@ get_user_manager_context = contextlib.asynccontextmanager(get_user_manager)
283
367
  async def _create_first_user(
284
368
  email: str,
285
369
  password: str,
370
+ project_dir: str,
371
+ profile_id: int | None = None,
286
372
  is_superuser: bool = False,
287
373
  is_verified: bool = False,
288
374
  ) -> None:
@@ -331,6 +417,8 @@ async def _create_first_user(
331
417
  kwargs = dict(
332
418
  email=email,
333
419
  password=password,
420
+ project_dir=project_dir,
421
+ profile_id=profile_id,
334
422
  is_superuser=is_superuser,
335
423
  is_verified=is_verified,
336
424
  )
@@ -2,47 +2,65 @@ import smtplib
2
2
  from email.message import EmailMessage
3
3
  from email.utils import formataddr
4
4
 
5
- from cryptography.fernet import Fernet
6
-
7
5
  from fractal_server.config import PublicEmailSettings
6
+ from fractal_server.logger import set_logger
7
+
8
+ logger = set_logger(__name__)
8
9
 
9
10
 
10
- def mail_new_oauth_signup(msg: str, email_settings: PublicEmailSettings):
11
+ def send_fractal_email_or_log_failure(
12
+ *,
13
+ subject: str,
14
+ msg: str,
15
+ email_settings: PublicEmailSettings | None,
16
+ ):
11
17
  """
12
- Send an email using the specified settings to notify a new OAuth signup.
18
+ Send an email using the specified settings, or log about failure.
13
19
  """
14
20
 
15
- mail_msg = EmailMessage()
16
- mail_msg.set_content(msg)
17
- mail_msg["From"] = formataddr(
18
- (email_settings.sender, email_settings.sender)
19
- )
20
- mail_msg["To"] = ", ".join(
21
- [
22
- formataddr((recipient, recipient))
23
- for recipient in email_settings.recipients
24
- ]
25
- )
26
- mail_msg[
27
- "Subject"
28
- ] = f"[Fractal, {email_settings.instance_name}] New OAuth signup"
21
+ if email_settings is None:
22
+ logger.error(
23
+ f"Cannot send email with {subject=}, because {email_settings=}."
24
+ )
29
25
 
30
- with smtplib.SMTP(
31
- email_settings.smtp_server, email_settings.port
32
- ) as server:
33
- server.ehlo()
34
- if email_settings.use_starttls:
35
- server.starttls()
26
+ try:
27
+ logger.info(f"START sending email with {subject=}.")
28
+ mail_msg = EmailMessage()
29
+ mail_msg.set_content(msg)
30
+ mail_msg["From"] = formataddr(
31
+ (email_settings.sender, email_settings.sender)
32
+ )
33
+ mail_msg["To"] = ", ".join(
34
+ [
35
+ formataddr((recipient, recipient))
36
+ for recipient in email_settings.recipients
37
+ ]
38
+ )
39
+ mail_msg[
40
+ "Subject"
41
+ ] = f"[Fractal, {email_settings.instance_name}] {subject}"
42
+ with smtplib.SMTP(
43
+ email_settings.smtp_server,
44
+ email_settings.port,
45
+ ) as server:
36
46
  server.ehlo()
37
- if email_settings.use_login:
38
- password = (
39
- Fernet(email_settings.encryption_key.get_secret_value())
40
- .decrypt(email_settings.encrypted_password.get_secret_value())
41
- .decode("utf-8")
47
+ if email_settings.use_starttls:
48
+ server.starttls()
49
+ server.ehlo()
50
+ if email_settings.use_login:
51
+ server.login(
52
+ user=email_settings.sender,
53
+ password=email_settings.password.get_secret_value(),
54
+ )
55
+ server.sendmail(
56
+ from_addr=email_settings.sender,
57
+ to_addrs=email_settings.recipients,
58
+ msg=mail_msg.as_string(),
42
59
  )
43
- server.login(user=email_settings.sender, password=password)
44
- server.sendmail(
45
- from_addr=email_settings.sender,
46
- to_addrs=email_settings.recipients,
47
- msg=mail_msg.as_string(),
60
+ logger.info(f"END sending email with {subject=}.")
61
+
62
+ except Exception as e:
63
+ logger.error(
64
+ "Could not send self-registration email, "
65
+ f"original error: {str(e)}."
48
66
  )
@@ -1,3 +1,5 @@
1
+ from ._data import DataAuthScheme # noqa F401
2
+ from ._data import DataSettings
1
3
  from ._database import DatabaseSettings
2
4
  from ._email import EmailSettings
3
5
  from ._email import PublicEmailSettings # noqa F401
@@ -19,3 +21,7 @@ def get_email_settings(email_settings=EmailSettings()) -> EmailSettings:
19
21
 
20
22
  def get_oauth_settings(oauth_settings=OAuthSettings()) -> OAuthSettings:
21
23
  return oauth_settings
24
+
25
+
26
+ def get_data_settings(data_settings=DataSettings()) -> DataSettings:
27
+ return data_settings
@@ -0,0 +1,68 @@
1
+ from enum import StrEnum
2
+ from typing import Self
3
+
4
+ from pydantic import model_validator
5
+ from pydantic_settings import BaseSettings
6
+ from pydantic_settings import SettingsConfigDict
7
+
8
+ from ._settings_config import SETTINGS_CONFIG_DICT
9
+ from fractal_server.types import AbsolutePathStr
10
+
11
+
12
+ class DataAuthScheme(StrEnum):
13
+ VIEWER_PATHS = "viewer-paths"
14
+ USERS_FOLDERS = "users-folders"
15
+ NONE = "none"
16
+
17
+
18
+ class DataSettings(BaseSettings):
19
+ """
20
+ Settings for the `fractal-data` integration.
21
+ """
22
+
23
+ model_config = SettingsConfigDict(**SETTINGS_CONFIG_DICT)
24
+
25
+ FRACTAL_DATA_AUTH_SCHEME: DataAuthScheme = "none"
26
+ """
27
+ Defines how the list of allowed viewer paths is built.
28
+
29
+ This variable affects the `GET /auth/current-user/allowed-viewer-paths/`
30
+ response, which is then consumed by
31
+ [fractal-data](https://github.com/fractal-analytics-platform/fractal-data).
32
+
33
+ Options:
34
+
35
+ - "viewer-paths": The list of allowed viewer paths will include the user's
36
+ `project_dir` along with any path defined in user groups' `viewer_paths`
37
+ attributes.
38
+ - "users-folders": The list will consist of the user's `project_dir` and a
39
+ user-specific folder. The user folder is constructed by concatenating
40
+ the base folder `FRACTAL_DATA_BASE_FOLDER` with the user's profile
41
+ `username`.
42
+ - "none": An empty list will be returned, indicating no access to
43
+ viewer paths. Useful when vizarr viewer is not used.
44
+ """
45
+
46
+ FRACTAL_DATA_BASE_FOLDER: AbsolutePathStr | None = None
47
+ """
48
+ Base path to Zarr files that will be served by fractal-vizarr-viewer;
49
+ This variable is required and used only when
50
+ FRACTAL_DATA_AUTHORIZATION_SCHEME is set to "users-folders".
51
+ """
52
+
53
+ @model_validator(mode="after")
54
+ def check(self: Self) -> Self:
55
+ """
56
+ `FRACTAL_DATA_BASE_FOLDER` is required when
57
+ `FRACTAL_DATA_AUTHORIZATION_SCHEME` is set to `"users-folders"`.
58
+ """
59
+ if (
60
+ self.FRACTAL_DATA_AUTH_SCHEME == DataAuthScheme.USERS_FOLDERS
61
+ and self.FRACTAL_DATA_BASE_FOLDER is None
62
+ ):
63
+ raise ValueError(
64
+ "FRACTAL_DATA_BASE_FOLDER is required when "
65
+ "FRACTAL_DATA_AUTH_SCHEME is set to "
66
+ "users-folders"
67
+ )
68
+ return self