Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Allows to overwrite the defaults for any FrontendUserPreference ⚠️ #5716

Merged
6 changes: 3 additions & 3 deletions .env-devel
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ WEBSERVER_PRODUCTS=1
WEBSERVER_PUBLICATIONS=1
WEBSERVER_SOCKETIO=1
WEBSERVER_TAGS=1
WEBSERVER_USERS=1
WEBSERVER_USERS={}
WEBSERVER_VERSION_CONTROL=1

# For development ONLY ---------------
Expand Down Expand Up @@ -323,7 +323,7 @@ WB_GC_STATICWEB=null
WB_GC_STUDIES_DISPATCHER=null
WB_GC_TAGS=0
WB_GC_TRACING=null
WB_GC_USERS=0
WB_GC_USERS={}
WB_GC_VERSION_CONTROL=0
WB_GC_WALLETS=0

Expand Down Expand Up @@ -364,6 +364,6 @@ WB_DB_EL_STORAGE=null
WB_DB_EL_STUDIES_DISPATCHER=null
WB_DB_EL_TAGS=0
WB_DB_EL_TRACING=null
WB_DB_EL_USERS=0
WB_DB_EL_USERS={}
WB_DB_EL_VERSION_CONTROL=0
WB_DB_EL_WALLETS=0
17 changes: 17 additions & 0 deletions packages/models-library/src/models_library/user_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,26 @@ class FrontendUserPreference(_BaseUserPreferenceModel):
..., description="used by the frontend"
)

value: Any

def to_db(self) -> dict:
return self.dict(exclude={"preference_identifier", "preference_type"})

@classmethod
def update_preference_default_value(cls, new_default: Any) -> None:
expected_type = cls.__fields__["value"].type_
detected_type = type(new_default)
if expected_type != detected_type:
msg = (
f"Error, {cls.__name__} {expected_type=} differs from {detected_type=}"
)
raise TypeError(msg)

if cls.__fields__["value"].default is None:
cls.__fields__["value"].default_factory = lambda: new_default
else:
cls.__fields__["value"].default = new_default


class UserServiceUserPreference(_BaseUserPreferenceModel):
preference_type: PreferenceType = Field(PreferenceType.USER_SERVICE, const=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from .statics.settings import FrontEndAppSettings, StaticWebserverModuleSettings
from .storage.settings import StorageSettings
from .studies_dispatcher.settings import StudiesDispatcherSettings
from .users.settings import UsersSettings

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -211,6 +212,9 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings):
WEBSERVER_RABBITMQ: RabbitSettings | None = Field(
auto_default_from_env=True, description="rabbitmq plugin"
)
WEBSERVER_USERS: UsersSettings | None = Field(
auto_default_from_env=True, description="users plugin"
)

# These plugins only require (for the moment) an entry to toggle between enabled/disabled
WEBSERVER_ANNOUNCEMENTS: bool = False
Expand All @@ -225,7 +229,6 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings):
WEBSERVER_REMOTE_DEBUG: bool = True
WEBSERVER_SOCKETIO: bool = True
WEBSERVER_TAGS: bool = True
WEBSERVER_USERS: bool = True
WEBSERVER_VERSION_CONTROL: bool = True
WEBSERVER_WALLETS: bool = True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ def convert_to_app_config(app_settings: ApplicationSettings) -> dict[str, Any]:
"port": app_settings.WEBSERVER_PORT,
"log_level": f"{app_settings.WEBSERVER_LOGLEVEL}",
"testing": False, # TODO: deprecate!
"studies_access_enabled": int(
app_settings.WEBSERVER_STUDIES_DISPATCHER.STUDIES_ACCESS_ANONYMOUS_ALLOWED
)
if app_settings.WEBSERVER_STUDIES_DISPATCHER
else 0,
"studies_access_enabled": (
int(
app_settings.WEBSERVER_STUDIES_DISPATCHER.STUDIES_ACCESS_ANONYMOUS_ALLOWED
)
if app_settings.WEBSERVER_STUDIES_DISPATCHER
else 0
),
},
"tracing": {
"enabled": 1 if app_settings.WEBSERVER_TRACING is not None else 0,
Expand Down Expand Up @@ -83,23 +85,31 @@ def convert_to_app_config(app_settings: ApplicationSettings) -> dict[str, Any]:
# -----------------------------
"login": {
"enabled": app_settings.WEBSERVER_LOGIN is not None,
"registration_invitation_required": 1
if getattr(
app_settings.WEBSERVER_LOGIN,
"LOGIN_REGISTRATION_INVITATION_REQUIRED",
None,
)
else 0,
"registration_confirmation_required": 1
if getattr(
app_settings.WEBSERVER_LOGIN,
"LOGIN_REGISTRATION_CONFIRMATION_REQUIRED",
None,
)
else 0,
"password_min_length": 12
if getattr(app_settings.WEBSERVER_LOGIN, "LOGIN_PASSWORD_MIN_LENGTH", None)
else 0,
"registration_invitation_required": (
1
if getattr(
app_settings.WEBSERVER_LOGIN,
"LOGIN_REGISTRATION_INVITATION_REQUIRED",
None,
)
else 0
),
"registration_confirmation_required": (
1
if getattr(
app_settings.WEBSERVER_LOGIN,
"LOGIN_REGISTRATION_CONFIRMATION_REQUIRED",
None,
)
else 0
),
"password_min_length": (
12
if getattr(
app_settings.WEBSERVER_LOGIN, "LOGIN_PASSWORD_MIN_LENGTH", None
)
else 0
),
},
"smtp": {
"host": getattr(app_settings.WEBSERVER_EMAIL, "SMTP_HOST", None),
Expand Down Expand Up @@ -164,7 +174,7 @@ def convert_to_app_config(app_settings: ApplicationSettings) -> dict[str, Any]:
"enabled": app_settings.WEBSERVER_STUDIES_DISPATCHER is not None
},
"tags": {"enabled": app_settings.WEBSERVER_TAGS},
"users": {"enabled": app_settings.WEBSERVER_USERS},
"users": {"enabled": app_settings.WEBSERVER_USERS is not None},
"version_control": {"enabled": app_settings.WEBSERVER_VERSION_CONTROL},
"wallets": {"enabled": app_settings.WEBSERVER_WALLETS},
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Final

from aiohttp import web
from models_library.authentification import TwoFactorAuthentificationMethod
from models_library.shared_user_preferences import (
AllowMetricsCollectionFrontendUserPreference,
Expand All @@ -11,6 +12,8 @@
)
from pydantic import Field, NonNegativeInt

from .settings import UsersSettings, get_plugin_settings

_MINUTE: Final[NonNegativeInt] = 60


Expand Down Expand Up @@ -143,3 +146,17 @@ def get_preference_name(preference_identifier: PreferenceIdentifier) -> Preferen

def get_preference_identifier(preference_name: PreferenceName) -> PreferenceIdentifier:
return _PREFERENCE_NAME_TO_IDENTIFIER_MAPPING[preference_name]


def overwrite_user_preferences_defaults(app: web.Application) -> None:
settings: UsersSettings = get_plugin_settings(app)

search_map: dict[str, type[FrontendUserPreference]] = {
x.__name__: x for x in ALL_FRONTEND_PREFERENCES
}

for (
preference_class,
value,
) in settings.USERS_FRONTEND_PREFERENCES_DEFAULTS_OVERWRITES.items():
search_map[preference_class].update_preference_default_value(value)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" users management subsystem

"""

import logging

from aiohttp import web
Expand All @@ -14,6 +15,7 @@
_preferences_handlers,
_tokens_handlers,
)
from ._preferences_models import overwrite_user_preferences_defaults

_logger = logging.getLogger(__name__)

Expand All @@ -28,6 +30,7 @@
def setup_users(app: web.Application):
assert app[APP_SETTINGS_KEY].WEBSERVER_USERS # nosec
setup_observer_registry(app)
overwrite_user_preferences_defaults(app)

app.router.add_routes(_handlers.routes)
app.router.add_routes(_tokens_handlers.routes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from ._preferences_models import (
PreferredWalletIdFrontendUserPreference,
TwoFAFrontendUserPreference,
UserInactivityThresholdFrontendUserPreference,
)
from .exceptions import UserDefaultWalletNotFoundError

Expand All @@ -12,5 +11,4 @@
"TwoFAFrontendUserPreference",
"set_frontend_user_preference",
"UserDefaultWalletNotFoundError",
"UserInactivityThresholdFrontendUserPreference",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from aiohttp import web
from pydantic import Field, StrictBool, StrictFloat, StrictInt, StrictStr
from settings_library.base import BaseCustomSettings
from settings_library.utils_service import MixinServiceSettings

from .._constants import APP_SETTINGS_KEY


class UsersSettings(BaseCustomSettings, MixinServiceSettings):
USERS_FRONTEND_PREFERENCES_DEFAULTS_OVERWRITES: dict[
str, StrictInt | StrictFloat | StrictStr | StrictBool | list | dict | None
] = Field(
default_factory=dict,
description="key: name of the FrontendUserPreference, value: new default",
)


def get_plugin_settings(app: web.Application) -> UsersSettings:
settings = app[APP_SETTINGS_KEY].WEBSERVER_USERS
assert settings, "setup_settings not called?" # nosec
assert isinstance(settings, UsersSettings) # nosec
return settings
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def app_environment(
"WEBSERVER_SOCKETIO": "0",
"WEBSERVER_TAGS": "1",
"WEBSERVER_TRACING": "null",
"WEBSERVER_USERS": "1",
"WEBSERVER_VERSION_CONTROL": "0",
"WEBSERVER_WALLETS": "0",
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc
"WEBSERVER_STORAGE": "null",
"WEBSERVER_TAGS": "1",
"WEBSERVER_TRACING": "null",
"WEBSERVER_USERS": "1",
"WEBSERVER_VERSION_CONTROL": "0",
"WEBSERVER_WALLETS": "0",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc
"WEBSERVER_STUDIES_DISPATCHER": "null",
"WEBSERVER_TAGS": "1",
"WEBSERVER_TRACING": "null",
"WEBSERVER_USERS": "1",
"WEBSERVER_VERSION_CONTROL": "0",
"WEBSERVER_WALLETS": "1",
},
Expand Down
1 change: 0 additions & 1 deletion services/web/server/tests/unit/with_dbs/03/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc
"WEBSERVER_STUDIES_DISPATCHER": "null",
"WEBSERVER_TAGS": "1",
"WEBSERVER_TRACING": "null",
"WEBSERVER_USERS": "1",
"WEBSERVER_VERSION_CONTROL": "0",
"WEBSERVER_WALLETS": "0",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ def app_environment(app_environment: EnvVarsDict, monkeypatch: pytest.MonkeyPatc
"WEBSERVER_TRACING": "null",
"WEBSERVER_DIRECTOR_V2": "null",
"WEBSERVER_CATALOG": "null",
"WEBSERVER_USERS": "True",
"WEBSERVER_REDIS": "null",
"WEBSERVER_SCICRUNCH": "null",
"WEBSERVER_VERSION_CONTROL": "0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
from models_library.user_preferences import PreferenceIdentifier, PreferenceName
# pylint:disable=redefined-outer-name
# pylint:disable=unused-argument

import json
from typing import Any
from unittest.mock import Mock

import pytest
from aiohttp import web
from aiohttp.test_utils import TestClient
from models_library.user_preferences import (
FrontendUserPreference,
PreferenceIdentifier,
PreferenceName,
)
from pytest_simcore.helpers.utils_envs import EnvVarsDict, setenvs_from_dict
from simcore_service_webserver._constants import APP_SETTINGS_KEY
from simcore_service_webserver.application_settings import ApplicationSettings
from simcore_service_webserver.users._preferences_models import (
ALL_FRONTEND_PREFERENCES,
TelemetryLowDiskSpaceWarningThresholdFrontendUserPreference,
get_preference_identifier,
get_preference_name,
overwrite_user_preferences_defaults,
)


Expand All @@ -23,3 +42,75 @@ def test_get_preference_name_and_get_preference_identifier():
preference_identifier
)
assert preference_name_via_identifier == preference_name


@pytest.fixture
def app_environment(
app_environment: EnvVarsDict,
monkeypatch: pytest.MonkeyPatch,
overwrites: dict[str, Any],
) -> EnvVarsDict:
return app_environment | setenvs_from_dict(
monkeypatch,
{
"WEBSERVER_USERS": json.dumps(
{"USERS_FRONTEND_PREFERENCES_DEFAULTS_OVERWRITES": overwrites}
)
},
)


@pytest.fixture
def app(client: TestClient) -> web.Application:
assert client.app
return client.app


@pytest.mark.parametrize(
"overwrites",
[
{
"UserInactivityThresholdFrontendUserPreference": 45,
"WalletIndicatorVisibilityFrontendUserPreference": "nothing",
"ServicesFrontendUserPreference": {"empty": "data"},
"DoNotShowAnnouncementsFrontendUserPreference": [1, 5, 70],
"ConnectPortsAutomaticallyFrontendUserPreference": False,
}
],
)
def test_overwrite_user_preferences_defaults(
app: web.Application, overwrites: dict[str, Any]
):
search_map: dict[str, type[FrontendUserPreference]] = {
x.__name__: x for x in ALL_FRONTEND_PREFERENCES
}
for class_name, expected_default in overwrites.items():
instance = search_map[class_name]()
assert instance.value == expected_default


@pytest.fixture
def mock_app(app_environment: EnvVarsDict) -> Mock:
app = {APP_SETTINGS_KEY: Mock()}
app[APP_SETTINGS_KEY] = ApplicationSettings.create_from_envs()
return app # type: ignore


@pytest.mark.parametrize(
"overwrites",
[
{"WalletIndicatorVisibilityFrontendUserPreference": 34},
{"UserInactivityThresholdFrontendUserPreference": 34.6},
{"ServicesFrontendUserPreference": [1, 3, 4]},
{"ServicesFrontendUserPreference": "str"},
{"ServicesFrontendUserPreference": 1},
{"DoNotShowAnnouncementsFrontendUserPreference": {}},
{"DoNotShowAnnouncementsFrontendUserPreference": 1},
{"DoNotShowAnnouncementsFrontendUserPreference": 3.4},
],
)
def test_overwrite_user_preferences_defaults_wrong_type(
mock_app: Mock, overwrites: dict[str, Any]
):
with pytest.raises(TypeError):
overwrite_user_preferences_defaults(mock_app)
Loading