fractal-server 2.17.0a3__py3-none-any.whl → 2.17.0a5__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.
- fractal_server/__init__.py +1 -1
- fractal_server/__main__.py +137 -120
- fractal_server/app/models/security.py +19 -21
- fractal_server/app/models/user_settings.py +1 -0
- fractal_server/app/models/v2/task_group.py +1 -0
- fractal_server/app/routes/admin/v2/accounting.py +3 -3
- fractal_server/app/routes/admin/v2/impersonate.py +2 -2
- fractal_server/app/routes/admin/v2/job.py +6 -6
- fractal_server/app/routes/admin/v2/profile.py +4 -4
- fractal_server/app/routes/admin/v2/project.py +2 -2
- fractal_server/app/routes/admin/v2/resource.py +42 -8
- fractal_server/app/routes/admin/v2/task.py +2 -2
- fractal_server/app/routes/admin/v2/task_group.py +5 -5
- fractal_server/app/routes/admin/v2/task_group_lifecycle.py +4 -4
- fractal_server/app/routes/api/__init__.py +5 -5
- fractal_server/app/routes/api/v2/dataset.py +10 -19
- fractal_server/app/routes/api/v2/history.py +8 -8
- fractal_server/app/routes/api/v2/images.py +5 -5
- fractal_server/app/routes/api/v2/job.py +8 -8
- fractal_server/app/routes/api/v2/pre_submission_checks.py +3 -3
- fractal_server/app/routes/api/v2/project.py +6 -6
- fractal_server/app/routes/api/v2/status_legacy.py +2 -2
- fractal_server/app/routes/api/v2/submit.py +24 -26
- fractal_server/app/routes/api/v2/task.py +6 -7
- fractal_server/app/routes/api/v2/task_collection.py +4 -3
- fractal_server/app/routes/api/v2/task_collection_custom.py +4 -3
- fractal_server/app/routes/api/v2/task_collection_pixi.py +2 -2
- fractal_server/app/routes/api/v2/task_group.py +6 -6
- fractal_server/app/routes/api/v2/task_group_lifecycle.py +4 -4
- fractal_server/app/routes/api/v2/task_version_update.py +3 -3
- fractal_server/app/routes/api/v2/workflow.py +9 -9
- fractal_server/app/routes/api/v2/workflow_import.py +2 -2
- fractal_server/app/routes/api/v2/workflowtask.py +5 -5
- fractal_server/app/routes/auth/__init__.py +34 -5
- fractal_server/app/routes/auth/current_user.py +22 -67
- fractal_server/app/routes/auth/group.py +8 -35
- fractal_server/app/routes/auth/register.py +2 -2
- fractal_server/app/routes/auth/users.py +5 -46
- fractal_server/app/schemas/__init__.py +0 -1
- fractal_server/app/schemas/user.py +23 -0
- fractal_server/app/schemas/v2/task_group.py +1 -0
- fractal_server/app/security/__init__.py +134 -46
- fractal_server/app/security/signup_email.py +52 -34
- fractal_server/config/__init__.py +1 -7
- fractal_server/config/_email.py +10 -47
- fractal_server/config/_main.py +14 -3
- fractal_server/migrations/versions/f65ee53991e3_user_settings_related.py +67 -0
- fractal_server/runner/config/_slurm.py +3 -2
- fractal_server/runner/executors/slurm_common/base_slurm_runner.py +2 -2
- fractal_server/runner/executors/slurm_common/get_slurm_config.py +1 -1
- fractal_server/runner/executors/slurm_common/slurm_config.py +7 -13
- fractal_server/runner/executors/slurm_ssh/runner.py +1 -1
- fractal_server/runner/executors/slurm_sudo/runner.py +1 -1
- fractal_server/runner/v2/_slurm_ssh.py +2 -1
- fractal_server/runner/v2/_slurm_sudo.py +1 -1
- fractal_server/runner/v2/submit_workflow.py +12 -12
- {fractal_server-2.17.0a3.dist-info → fractal_server-2.17.0a5.dist-info}/METADATA +1 -2
- {fractal_server-2.17.0a3.dist-info → fractal_server-2.17.0a5.dist-info}/RECORD +61 -64
- fractal_server/app/routes/aux/validate_user_settings.py +0 -76
- fractal_server/app/schemas/user_settings.py +0 -63
- fractal_server/app/user_settings.py +0 -32
- fractal_server/config/_init_data.py +0 -27
- {fractal_server-2.17.0a3.dist-info → fractal_server-2.17.0a5.dist-info}/WHEEL +0 -0
- {fractal_server-2.17.0a3.dist-info → fractal_server-2.17.0a5.dist-info}/entry_points.txt +0 -0
- {fractal_server-2.17.0a3.dist-info → fractal_server-2.17.0a5.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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
|
|
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
|
|
18
|
+
Send an email using the specified settings, or log about failure.
|
|
13
19
|
"""
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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,8 +1,8 @@
|
|
|
1
1
|
from ._database import DatabaseSettings
|
|
2
2
|
from ._email import EmailSettings
|
|
3
3
|
from ._email import PublicEmailSettings # noqa F401
|
|
4
|
-
from ._init_data import InitDataSettings
|
|
5
4
|
from ._main import Settings
|
|
5
|
+
from ._main import ViewerAuthScheme # noqa F401
|
|
6
6
|
from ._oauth import OAuthSettings
|
|
7
7
|
|
|
8
8
|
|
|
@@ -18,11 +18,5 @@ def get_email_settings(email_settings=EmailSettings()) -> EmailSettings:
|
|
|
18
18
|
return email_settings
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def get_init_data_settings(
|
|
22
|
-
init_data_settings=InitDataSettings(),
|
|
23
|
-
) -> InitDataSettings:
|
|
24
|
-
return init_data_settings
|
|
25
|
-
|
|
26
|
-
|
|
27
21
|
def get_oauth_settings(oauth_settings=OAuthSettings()) -> OAuthSettings:
|
|
28
22
|
return oauth_settings
|
fractal_server/config/_email.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
|
+
from typing import Self
|
|
2
3
|
|
|
3
|
-
from cryptography.fernet import Fernet
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
from pydantic import EmailStr
|
|
6
6
|
from pydantic import Field
|
|
@@ -31,8 +31,7 @@ class PublicEmailSettings(BaseModel):
|
|
|
31
31
|
recipients: list[EmailStr] = Field(min_length=1)
|
|
32
32
|
smtp_server: str
|
|
33
33
|
port: int
|
|
34
|
-
|
|
35
|
-
encryption_key: SecretStr | None = None
|
|
34
|
+
password: SecretStr | None = None
|
|
36
35
|
instance_name: str
|
|
37
36
|
use_starttls: bool
|
|
38
37
|
use_login: bool
|
|
@@ -53,10 +52,6 @@ class EmailSettings(BaseSettings):
|
|
|
53
52
|
"""
|
|
54
53
|
Password for the OAuth-signup email sender.
|
|
55
54
|
"""
|
|
56
|
-
FRACTAL_EMAIL_PASSWORD_KEY: SecretStr | None = None
|
|
57
|
-
"""
|
|
58
|
-
Key value for `cryptography.fernet` decrypt
|
|
59
|
-
"""
|
|
60
55
|
FRACTAL_EMAIL_SMTP_SERVER: str | None = None
|
|
61
56
|
"""
|
|
62
57
|
SMTP server for the OAuth-signup emails.
|
|
@@ -81,8 +76,7 @@ class EmailSettings(BaseSettings):
|
|
|
81
76
|
FRACTAL_EMAIL_USE_LOGIN: Literal["true", "false"] = "true"
|
|
82
77
|
"""
|
|
83
78
|
Whether to use login when using the SMTP server.
|
|
84
|
-
If 'true', FRACTAL_EMAIL_PASSWORD
|
|
85
|
-
provided.
|
|
79
|
+
If 'true', FRACTAL_EMAIL_PASSWORD must be provided.
|
|
86
80
|
Accepted values: 'true', 'false'.
|
|
87
81
|
"""
|
|
88
82
|
|
|
@@ -93,7 +87,7 @@ class EmailSettings(BaseSettings):
|
|
|
93
87
|
"""
|
|
94
88
|
|
|
95
89
|
@model_validator(mode="after")
|
|
96
|
-
def validate_email_settings(self):
|
|
90
|
+
def validate_email_settings(self: Self) -> Self:
|
|
97
91
|
"""
|
|
98
92
|
Set `self.public`.
|
|
99
93
|
"""
|
|
@@ -119,49 +113,18 @@ class EmailSettings(BaseSettings):
|
|
|
119
113
|
use_starttls = self.FRACTAL_EMAIL_USE_STARTTLS == "true"
|
|
120
114
|
use_login = self.FRACTAL_EMAIL_USE_LOGIN == "true"
|
|
121
115
|
|
|
122
|
-
if use_login:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
)
|
|
128
|
-
if self.FRACTAL_EMAIL_PASSWORD_KEY is None:
|
|
129
|
-
raise ValueError(
|
|
130
|
-
"'FRACTAL_EMAIL_USE_LOGIN' is 'true' but "
|
|
131
|
-
"'FRACTAL_EMAIL_PASSWORD_KEY' is not provided."
|
|
132
|
-
)
|
|
133
|
-
try:
|
|
134
|
-
(
|
|
135
|
-
Fernet(
|
|
136
|
-
self.FRACTAL_EMAIL_PASSWORD_KEY.get_secret_value()
|
|
137
|
-
)
|
|
138
|
-
.decrypt(
|
|
139
|
-
self.FRACTAL_EMAIL_PASSWORD.get_secret_value()
|
|
140
|
-
)
|
|
141
|
-
.decode("utf-8")
|
|
142
|
-
)
|
|
143
|
-
except Exception as e:
|
|
144
|
-
raise ValueError(
|
|
145
|
-
"Invalid pair (FRACTAL_EMAIL_PASSWORD, "
|
|
146
|
-
"FRACTAL_EMAIL_PASSWORD_KEY). "
|
|
147
|
-
f"Original error: {str(e)}."
|
|
148
|
-
)
|
|
149
|
-
password = self.FRACTAL_EMAIL_PASSWORD.get_secret_value()
|
|
150
|
-
else:
|
|
151
|
-
password = None
|
|
152
|
-
|
|
153
|
-
if self.FRACTAL_EMAIL_PASSWORD_KEY is not None:
|
|
154
|
-
key = self.FRACTAL_EMAIL_PASSWORD_KEY.get_secret_value()
|
|
155
|
-
else:
|
|
156
|
-
key = None
|
|
116
|
+
if use_login and self.FRACTAL_EMAIL_PASSWORD is None:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
"'FRACTAL_EMAIL_USE_LOGIN' is 'true' but "
|
|
119
|
+
"'FRACTAL_EMAIL_PASSWORD' is not provided."
|
|
120
|
+
)
|
|
157
121
|
|
|
158
122
|
self.public = PublicEmailSettings(
|
|
159
123
|
sender=self.FRACTAL_EMAIL_SENDER,
|
|
160
124
|
recipients=self.FRACTAL_EMAIL_RECIPIENTS.split(","),
|
|
161
125
|
smtp_server=self.FRACTAL_EMAIL_SMTP_SERVER,
|
|
162
126
|
port=self.FRACTAL_EMAIL_SMTP_PORT,
|
|
163
|
-
|
|
164
|
-
encryption_key=key,
|
|
127
|
+
password=self.FRACTAL_EMAIL_PASSWORD,
|
|
165
128
|
instance_name=self.FRACTAL_EMAIL_INSTANCE_NAME,
|
|
166
129
|
use_starttls=use_starttls,
|
|
167
130
|
use_login=use_login,
|
fractal_server/config/_main.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from enum import StrEnum
|
|
2
3
|
from typing import Literal
|
|
3
4
|
from typing import TypeVar
|
|
4
5
|
|
|
6
|
+
from pydantic import HttpUrl
|
|
5
7
|
from pydantic import SecretStr
|
|
6
8
|
from pydantic_settings import BaseSettings
|
|
7
9
|
from pydantic_settings import SettingsConfigDict
|
|
@@ -14,6 +16,12 @@ class FractalConfigurationError(ValueError):
|
|
|
14
16
|
pass
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
class ViewerAuthScheme(StrEnum):
|
|
20
|
+
VIEWER_PATHS = "viewer-paths"
|
|
21
|
+
USERS_FOLDERS = "users-folders"
|
|
22
|
+
NONE = "none"
|
|
23
|
+
|
|
24
|
+
|
|
17
25
|
T = TypeVar("T")
|
|
18
26
|
|
|
19
27
|
|
|
@@ -72,9 +80,7 @@ class Settings(BaseSettings):
|
|
|
72
80
|
Waiting time for the shutdown phase of executors
|
|
73
81
|
"""
|
|
74
82
|
|
|
75
|
-
FRACTAL_VIEWER_AUTHORIZATION_SCHEME:
|
|
76
|
-
"viewer-paths", "users-folders", "none"
|
|
77
|
-
] = "none"
|
|
83
|
+
FRACTAL_VIEWER_AUTHORIZATION_SCHEME: ViewerAuthScheme = "none"
|
|
78
84
|
"""
|
|
79
85
|
Defines how the list of allowed viewer paths is built.
|
|
80
86
|
|
|
@@ -119,3 +125,8 @@ class Settings(BaseSettings):
|
|
|
119
125
|
"FRACTAL_VIEWER_AUTHORIZATION_SCHEME is set to "
|
|
120
126
|
"users-folders"
|
|
121
127
|
)
|
|
128
|
+
|
|
129
|
+
FRACTAL_HELP_URL: HttpUrl | None = None
|
|
130
|
+
"""
|
|
131
|
+
The URL of an instance-specific Fractal help page.
|
|
132
|
+
"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""user-settings-related
|
|
2
|
+
|
|
3
|
+
Revision ID: f65ee53991e3
|
|
4
|
+
Revises: 90f6508c6379
|
|
5
|
+
Create Date: 2025-10-22 15:16:42.217910
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
import sqlalchemy as sa
|
|
9
|
+
from alembic import op
|
|
10
|
+
from sqlalchemy.dialects import postgresql
|
|
11
|
+
|
|
12
|
+
# revision identifiers, used by Alembic.
|
|
13
|
+
revision = "f65ee53991e3"
|
|
14
|
+
down_revision = "90f6508c6379"
|
|
15
|
+
branch_labels = None
|
|
16
|
+
depends_on = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def upgrade() -> None:
|
|
20
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
21
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
|
22
|
+
batch_op.add_column(
|
|
23
|
+
sa.Column(
|
|
24
|
+
"project_dir",
|
|
25
|
+
sa.String(),
|
|
26
|
+
server_default="/PLACEHOLDER",
|
|
27
|
+
nullable=False,
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
batch_op.add_column(
|
|
31
|
+
sa.Column(
|
|
32
|
+
"slurm_accounts",
|
|
33
|
+
postgresql.ARRAY(sa.String()),
|
|
34
|
+
server_default="{}",
|
|
35
|
+
nullable=True,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
batch_op.drop_constraint(
|
|
39
|
+
batch_op.f("fk_user_oauth_user_settings_id_user_settings"),
|
|
40
|
+
type_="foreignkey",
|
|
41
|
+
)
|
|
42
|
+
batch_op.drop_column("user_settings_id")
|
|
43
|
+
|
|
44
|
+
# ### end Alembic commands ###
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def downgrade() -> None:
|
|
48
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
49
|
+
with op.batch_alter_table("user_oauth", schema=None) as batch_op:
|
|
50
|
+
batch_op.add_column(
|
|
51
|
+
sa.Column(
|
|
52
|
+
"user_settings_id",
|
|
53
|
+
sa.INTEGER(),
|
|
54
|
+
autoincrement=False,
|
|
55
|
+
nullable=True,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
batch_op.create_foreign_key(
|
|
59
|
+
batch_op.f("fk_user_oauth_user_settings_id_user_settings"),
|
|
60
|
+
"user_settings",
|
|
61
|
+
["user_settings_id"],
|
|
62
|
+
["id"],
|
|
63
|
+
)
|
|
64
|
+
batch_op.drop_column("slurm_accounts")
|
|
65
|
+
batch_op.drop_column("project_dir")
|
|
66
|
+
|
|
67
|
+
# ### end Alembic commands ###
|
|
@@ -3,6 +3,7 @@ from typing import Annotated
|
|
|
3
3
|
from pydantic import AfterValidator
|
|
4
4
|
from pydantic import BaseModel
|
|
5
5
|
from pydantic import ConfigDict
|
|
6
|
+
from pydantic import Field
|
|
6
7
|
from pydantic.types import PositiveInt
|
|
7
8
|
|
|
8
9
|
from fractal_server.runner.config.slurm_mem_to_MB import slurm_mem_to_MB
|
|
@@ -44,7 +45,7 @@ class _SlurmConfigSet(BaseModel):
|
|
|
44
45
|
nodelist: NonEmptyStr | None = None
|
|
45
46
|
time: NonEmptyStr | None = None
|
|
46
47
|
account: NonEmptyStr | None = None
|
|
47
|
-
extra_lines: list[NonEmptyStr]
|
|
48
|
+
extra_lines: list[NonEmptyStr] = Field(default_factory=list)
|
|
48
49
|
gpus: NonEmptyStr | None = None
|
|
49
50
|
|
|
50
51
|
|
|
@@ -125,4 +126,4 @@ class JobRunnerConfigSLURM(BaseModel):
|
|
|
125
126
|
default_slurm_config: _SlurmConfigSet
|
|
126
127
|
gpu_slurm_config: _SlurmConfigSet | None = None
|
|
127
128
|
batching_config: _BatchingConfigSet
|
|
128
|
-
user_local_exports: DictStrStr
|
|
129
|
+
user_local_exports: DictStrStr = Field(default_factory=dict)
|
|
@@ -87,7 +87,7 @@ class BaseSlurmRunner(BaseRunner):
|
|
|
87
87
|
python_worker_interpreter: str,
|
|
88
88
|
poll_interval: int,
|
|
89
89
|
common_script_lines: list[str] | None = None,
|
|
90
|
-
user_cache_dir: str
|
|
90
|
+
user_cache_dir: str,
|
|
91
91
|
slurm_account: str | None = None,
|
|
92
92
|
):
|
|
93
93
|
self.slurm_runner_type = slurm_runner_type
|
|
@@ -252,7 +252,7 @@ class BaseSlurmRunner(BaseRunner):
|
|
|
252
252
|
f"Add {self.common_script_lines} to "
|
|
253
253
|
f"{new_slurm_config.extra_lines=}."
|
|
254
254
|
)
|
|
255
|
-
current_extra_lines = new_slurm_config.extra_lines
|
|
255
|
+
current_extra_lines = new_slurm_config.extra_lines
|
|
256
256
|
new_slurm_config.extra_lines = (
|
|
257
257
|
current_extra_lines + self.common_script_lines
|
|
258
258
|
)
|
|
@@ -77,7 +77,7 @@ def _get_slurm_config_internal(
|
|
|
77
77
|
else:
|
|
78
78
|
needs_gpu = False
|
|
79
79
|
logger.debug(f"[get_slurm_config] {needs_gpu=}")
|
|
80
|
-
if needs_gpu:
|
|
80
|
+
if needs_gpu and shared_config.gpu_slurm_config is not None:
|
|
81
81
|
for key, value in shared_config.gpu_slurm_config.model_dump(
|
|
82
82
|
exclude_unset=True, exclude={"mem"}
|
|
83
83
|
).items():
|
|
@@ -83,10 +83,10 @@ class SlurmConfig(BaseModel):
|
|
|
83
83
|
|
|
84
84
|
# Free-field attribute for extra lines to be added to the SLURM job
|
|
85
85
|
# preamble
|
|
86
|
-
extra_lines: list[str]
|
|
86
|
+
extra_lines: list[str] = Field(default_factory=list)
|
|
87
87
|
|
|
88
88
|
# Variables that will be `export`ed in the SLURM submission script
|
|
89
|
-
user_local_exports: dict[str, str]
|
|
89
|
+
user_local_exports: dict[str, str] = Field(default_factory=dict)
|
|
90
90
|
|
|
91
91
|
# Metaparameters needed to combine multiple tasks in each SLURM job
|
|
92
92
|
tasks_per_job: int | None = None
|
|
@@ -136,7 +136,7 @@ class SlurmConfig(BaseModel):
|
|
|
136
136
|
|
|
137
137
|
def to_sbatch_preamble(
|
|
138
138
|
self,
|
|
139
|
-
remote_export_dir: str
|
|
139
|
+
remote_export_dir: str,
|
|
140
140
|
) -> list[str]:
|
|
141
141
|
"""
|
|
142
142
|
Compile `SlurmConfig` object into the preamble of a SLURM submission
|
|
@@ -152,9 +152,8 @@ class SlurmConfig(BaseModel):
|
|
|
152
152
|
"SlurmConfig.sbatch_preamble requires that "
|
|
153
153
|
f"{self.parallel_tasks_per_job=} is not None."
|
|
154
154
|
)
|
|
155
|
-
if self.extra_lines:
|
|
156
|
-
|
|
157
|
-
raise ValueError(f"{self.extra_lines=} contains repetitions")
|
|
155
|
+
if len(self.extra_lines) != len(set(self.extra_lines)):
|
|
156
|
+
raise ValueError(f"{self.extra_lines=} contains repetitions")
|
|
158
157
|
|
|
159
158
|
mem_per_job_MB = self.parallel_tasks_per_job * self.mem_per_task_MB
|
|
160
159
|
lines = [
|
|
@@ -187,15 +186,10 @@ class SlurmConfig(BaseModel):
|
|
|
187
186
|
option = key.replace("_", "-")
|
|
188
187
|
lines.append(f"{self.prefix} --{option}={value}")
|
|
189
188
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
lines.append(line)
|
|
189
|
+
for line in self._sorted_extra_lines():
|
|
190
|
+
lines.append(line)
|
|
193
191
|
|
|
194
192
|
if self.user_local_exports:
|
|
195
|
-
if remote_export_dir is None:
|
|
196
|
-
raise ValueError(
|
|
197
|
-
f"remote_export_dir=None but {self.user_local_exports=}"
|
|
198
|
-
)
|
|
199
193
|
for key, value in self.user_local_exports.items():
|
|
200
194
|
tmp_value = str(Path(remote_export_dir) / value)
|
|
201
195
|
lines.append(f"export {key}={tmp_value}")
|