diff --git a/services/web/server/src/simcore_service_webserver/application.py b/services/web/server/src/simcore_service_webserver/application.py index 25132d38147..42cce73f79c 100644 --- a/services/web/server/src/simcore_service_webserver/application.py +++ b/services/web/server/src/simcore_service_webserver/application.py @@ -43,7 +43,7 @@ from .studies_dispatcher.plugin import setup_studies_dispatcher from .tags.plugin import setup_tags from .tracing import setup_app_tracing -from .users import setup_users +from .users.plugin import setup_users from .version_control.plugin import setup_version_control _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/exporter/formatters/formatter_v1.py b/services/web/server/src/simcore_service_webserver/exporter/formatters/formatter_v1.py index d2ad7ca689d..0533aa8eed5 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/formatters/formatter_v1.py +++ b/services/web/server/src/simcore_service_webserver/exporter/formatters/formatter_v1.py @@ -42,7 +42,7 @@ from ...projects.projects_api import get_project_for_user, submit_delete_project_task from ...projects.projects_db import APP_PROJECT_DBAPI, ProjectDBAPI from ...projects.projects_exceptions import ProjectsException -from ...users_api import get_user +from ...users.api import get_user from ...utils import now_str from ..exceptions import ExporterException from ..file_downloader import ParallelDownloader diff --git a/services/web/server/src/simcore_service_webserver/exporter/request_handlers.py b/services/web/server/src/simcore_service_webserver/exporter/request_handlers.py index 93897f7b493..43274b9cd66 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/request_handlers.py +++ b/services/web/server/src/simcore_service_webserver/exporter/request_handlers.py @@ -13,7 +13,7 @@ from ..projects.project_lock import lock_project from ..projects.projects_api import retrieve_and_notify_project_locked_state from ..security.decorators import permission_required -from ..users_api import get_user_name +from ..users.api import get_user_name from .exceptions import ExporterException from .export_import import study_duplicate, study_export, study_import from .formatters import FormatterV1 diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector_core.py b/services/web/server/src/simcore_service_webserver/garbage_collector_core.py index df51591d059..7bfd582b543 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector_core.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector_core.py @@ -17,7 +17,6 @@ from simcore_postgres_database.errors import DatabaseError from simcore_postgres_database.models.users import UserRole -from . import users_exceptions from .director.director_exceptions import DirectorException, ServiceNotFoundError from .director_v2 import api from .director_v2.exceptions import ServiceWaitingForManualIntervention @@ -38,13 +37,14 @@ ) from .redis import get_redis_lock_manager_client from .resource_manager.registry import RedisResourceRegistry, get_registry -from .users_api import ( - delete_user, +from .users import exceptions +from .users.api import ( + delete_user_without_projects, get_guest_user_ids_and_names, get_user, get_user_role, ) -from .users_exceptions import UserNotFoundError +from .users.exceptions import UserNotFoundError logger = logging.getLogger(__name__) @@ -471,7 +471,7 @@ async def _delete_all_projects_for_user(app: web.Application, user_id: int) -> N # recover user's primary_gid try: project_owner: dict = await get_user(app=app, user_id=user_id) - except users_exceptions.UserNotFoundError: + except exceptions.UserNotFoundError: logger.warning( "Could not recover user data for user '%s', stopping removal of projects!", f"{user_id=}", @@ -587,7 +587,7 @@ async def remove_guest_user_with_all_its_resources( "Deleting user %s because it is a GUEST", f"{user_id=}", ) - await delete_user(app, user_id) + await delete_user_without_projects(app, user_id) except ( DatabaseError, diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector_tasks_users.py b/services/web/server/src/simcore_service_webserver/garbage_collector_tasks_users.py index a71b0371826..b34b0cc725b 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector_tasks_users.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector_tasks_users.py @@ -18,7 +18,7 @@ from ._constants import APP_DB_ENGINE_KEY from .login.utils import notify_user_logout from .security.api import clean_auth_policy_cache -from .users_db import update_expired_users +from .users.api import update_expired_users logger = logging.getLogger(__name__) @@ -47,7 +47,7 @@ async def notify_user_logout_all_sessions( logger.warning( "Ignored error while notifying logout for %s", f"{user_id=}", - exec_info=True, + exc_info=True, extra=get_log_record_extra(user_id=user_id), ) diff --git a/services/web/server/src/simcore_service_webserver/garbage_collector_utils.py b/services/web/server/src/simcore_service_webserver/garbage_collector_utils.py index 59b61a8bb88..58d17dd62b2 100644 --- a/services/web/server/src/simcore_service_webserver/garbage_collector_utils.py +++ b/services/web/server/src/simcore_service_webserver/garbage_collector_utils.py @@ -6,14 +6,13 @@ from models_library.users import GroupID, UserID from simcore_postgres_database.errors import DatabaseError -from . import users_exceptions from .db_models import GroupType from .groups.api import get_group_from_gid from .projects.projects_db import APP_PROJECT_DBAPI, ProjectAccessRights from .projects.projects_exceptions import ProjectNotFoundError -from .users_api import get_user, get_user_id_from_gid -from .users_exceptions import UserNotFoundError -from .users_to_groups_api import get_users_for_gid +from .users import exceptions +from .users.api import get_user, get_user_id_from_gid, get_users_in_group +from .users.exceptions import UserNotFoundError logger = logging.getLogger(__name__) @@ -29,7 +28,9 @@ async def _fetch_new_project_owner_from_groups( # go through user_to_groups table and fetch all uid for matching gid for group_gid in standard_groups.keys(): # remove the current owner from the bunch - target_group_users = await get_users_for_gid(app=app, gid=group_gid) - {user_id} + target_group_users = await get_users_in_group(app=app, gid=group_gid) - { + user_id + } logger.info("Found group users '%s'", target_group_users) for possible_user_id in target_group_users: @@ -37,7 +38,7 @@ async def _fetch_new_project_owner_from_groups( try: possible_user = await get_user(app=app, user_id=possible_user_id) return int(possible_user["primary_gid"]) - except users_exceptions.UserNotFoundError: + except exceptions.UserNotFoundError: logger.warning( "Could not find new owner '%s' will try a new one", possible_user_id, diff --git a/services/web/server/src/simcore_service_webserver/groups/_db.py b/services/web/server/src/simcore_service_webserver/groups/_db.py index 68a5dfd2428..9762731e4f7 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_db.py +++ b/services/web/server/src/simcore_service_webserver/groups/_db.py @@ -10,13 +10,13 @@ from sqlalchemy.dialects.postgresql import insert from ..db_models import GroupType, groups, user_to_groups, users -from ..users_exceptions import UserNotFoundError +from ..users.exceptions import UserNotFoundError +from ._users import convert_user_in_group_to_schema from ._utils import ( AccessRightsDict, check_group_permissions, convert_groups_db_to_schema, convert_groups_schema_to_db, - convert_user_in_group_to_schema, ) from .exceptions import GroupNotFoundError, UserInGroupNotFoundError diff --git a/services/web/server/src/simcore_service_webserver/groups/_handlers.py b/services/web/server/src/simcore_service_webserver/groups/_handlers.py index c9d92b6c1af..79c34d146eb 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/groups/_handlers.py @@ -24,7 +24,7 @@ from ..scicrunch.models import ResearchResource, ResourceHit from ..scicrunch.service_client import SciCrunch from ..security.decorators import permission_required -from ..users_exceptions import UserNotFoundError +from ..users.exceptions import UserNotFoundError from . import api from ._classifiers import GroupClassifierRepository, build_rrids_tree_view from .exceptions import ( diff --git a/services/web/server/src/simcore_service_webserver/groups/_users.py b/services/web/server/src/simcore_service_webserver/groups/_users.py new file mode 100644 index 00000000000..c5aa3281fa9 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/groups/_users.py @@ -0,0 +1,15 @@ +""" +NOTE: Coupling with user's plugin api modules should be added here to avoid cyclic dependencies +""" + +from typing import Any, Mapping + +from ..users.schemas import convert_user_db_to_schema + + +def convert_user_in_group_to_schema(user: Mapping[str, Any]) -> dict[str, str]: + group_user = convert_user_db_to_schema(user) + group_user.pop("role") + group_user["accessRights"] = user["access_rights"] + group_user["gid"] = user["primary_gid"] + return group_user diff --git a/services/web/server/src/simcore_service_webserver/groups/_utils.py b/services/web/server/src/simcore_service_webserver/groups/_utils.py index 295a2c2ce61..46840e17373 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_utils.py +++ b/services/web/server/src/simcore_service_webserver/groups/_utils.py @@ -1,8 +1,7 @@ -from typing import Any, Mapping, TypedDict +from typing import TypedDict from aiopg.sa.result import RowProxy -from ..users_utils import convert_user_db_to_schema from .exceptions import UserInsufficientRightsError _GROUPS_SCHEMA_TO_DB = { @@ -48,11 +47,3 @@ def convert_groups_schema_to_db(schema: dict) -> dict: for k, v in _GROUPS_SCHEMA_TO_DB.items() if k in schema and k != "gid" } - - -def convert_user_in_group_to_schema(user: Mapping[str, Any]) -> dict[str, str]: - group_user = convert_user_db_to_schema(user) - group_user.pop("role") - group_user["accessRights"] = user["access_rights"] - group_user["gid"] = user["primary_gid"] - return group_user diff --git a/services/web/server/src/simcore_service_webserver/groups/api.py b/services/web/server/src/simcore_service_webserver/groups/api.py index f4cbe45baa2..675e9335188 100644 --- a/services/web/server/src/simcore_service_webserver/groups/api.py +++ b/services/web/server/src/simcore_service_webserver/groups/api.py @@ -5,7 +5,7 @@ from models_library.users import GroupID, UserID from ..db import get_database_engine -from ..users_api import get_user +from ..users.api import get_user from . import _db from ._utils import AccessRightsDict from .exceptions import GroupsException diff --git a/services/web/server/src/simcore_service_webserver/groups/plugin.py b/services/web/server/src/simcore_service_webserver/groups/plugin.py index ef1f75fe7ec..70b2f4eeb25 100644 --- a/services/web/server/src/simcore_service_webserver/groups/plugin.py +++ b/services/web/server/src/simcore_service_webserver/groups/plugin.py @@ -14,7 +14,7 @@ __name__, ModuleCategory.ADDON, settings_name="WEBSERVER_GROUPS", - depends=["simcore_service_webserver.rest", "simcore_service_webserver.users"], + depends=["simcore_service_webserver.rest"], logger=_logger, ) def setup_groups(app: web.Application): diff --git a/services/web/server/src/simcore_service_webserver/projects/_create_utils.py b/services/web/server/src/simcore_service_webserver/projects/_create_utils.py index 5f7f4443c20..dd940eb5f14 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_create_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/_create_utils.py @@ -21,7 +21,7 @@ copy_data_folders_from_project, get_project_total_size_simcore_s3, ) -from ..users_api import get_user_name +from ..users.api import get_user_name from . import projects_api from ._permalink import update_or_pop_permalink_in_project from ._rest_schemas import ProjectGet diff --git a/services/web/server/src/simcore_service_webserver/projects/_delete_utils.py b/services/web/server/src/simcore_service_webserver/projects/_delete_utils.py index d9533b0ed60..55fc9f8cbb4 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_delete_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/_delete_utils.py @@ -15,8 +15,8 @@ from ..director_v2 import api from ..storage.api import delete_data_folders_of_project -from ..users_api import UserNameDict -from ..users_exceptions import UserNotFoundError +from ..users.api import UserNameDict +from ..users.exceptions import UserNotFoundError from .projects_db import ProjectDBAPI from .projects_exceptions import ( ProjectDeleteError, diff --git a/services/web/server/src/simcore_service_webserver/projects/project_lock.py b/services/web/server/src/simcore_service_webserver/projects/project_lock.py index 6eb64169c11..73077a65119 100644 --- a/services/web/server/src/simcore_service_webserver/projects/project_lock.py +++ b/services/web/server/src/simcore_service_webserver/projects/project_lock.py @@ -1,7 +1,7 @@ import datetime from asyncio.log import logger from contextlib import asynccontextmanager -from typing import Final, Optional, Union +from typing import Final import redis from aiohttp import web @@ -11,7 +11,7 @@ from servicelib.background_task import periodic_task from ..redis import get_redis_lock_manager_client -from ..users_api import UserNameDict +from ..users.api import UserNameDict from .projects_exceptions import ProjectLockError PROJECT_REDIS_LOCK_KEY: str = "project_lock:{}" @@ -27,7 +27,7 @@ async def _auto_extend_project_lock(project_lock: Lock) -> None: @asynccontextmanager async def lock_project( app: web.Application, - project_uuid: Union[str, ProjectID], + project_uuid: str | ProjectID, status: ProjectStatus, user_id: int, user_name: UserNameDict, @@ -81,7 +81,7 @@ async def lock_project( async def is_project_locked( - app: web.Application, project_uuid: Union[str, ProjectID] + app: web.Application, project_uuid: str | ProjectID ) -> bool: redis_lock = get_redis_lock_manager_client(app).lock( PROJECT_REDIS_LOCK_KEY.format(project_uuid) @@ -90,8 +90,8 @@ async def is_project_locked( async def get_project_locked_state( - app: web.Application, project_uuid: Union[str, ProjectID] -) -> Optional[ProjectLocked]: + app: web.Application, project_uuid: str | ProjectID +) -> ProjectLocked | None: """returns the ProjectLocked object if the project is locked""" if await is_project_locked(app, project_uuid): redis_locks_client = get_redis_lock_manager_client(app) diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_api.py b/services/web/server/src/simcore_service_webserver/projects/projects_api.py index d86019fb543..b67600edad2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_api.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_api.py @@ -44,6 +44,7 @@ from servicelib.json_serialization import json_dumps from servicelib.logging_utils import get_log_record_extra, log_context from servicelib.utils import fire_and_forget_task, logged_gather +from simcore_postgres_database.models.users import UserRole from simcore_postgres_database.webserver_models import ProjectType from .. import catalog_client @@ -63,8 +64,8 @@ send_messages, ) from ..storage import api as storage_api -from ..users_api import UserRole, get_user_name, get_user_role -from ..users_exceptions import UserNotFoundError +from ..users.api import get_user_name, get_user_role +from ..users.exceptions import UserNotFoundError from . import _delete_utils, _nodes_utils from .project_lock import ( UserNameDict, diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py b/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py index af12da023d7..c06b5b0bf3a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py @@ -21,7 +21,7 @@ from sqlalchemy.sql import select from ..db_models import GroupType, groups, study_tags, user_to_groups, users -from ..users_exceptions import UserNotFoundError +from ..users.exceptions import UserNotFoundError from ..utils import format_datetime from .project_models import ProjectDict, ProjectProxy from .projects_exceptions import ProjectInvalidRightsError, ProjectNotFoundError diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py b/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py index f9a9acfa8db..94dd94308b2 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_handlers.py @@ -25,13 +25,13 @@ from simcore_postgres_database.models.users import UserRole from simcore_postgres_database.webserver_models import ProjectType -from .. import users_api from .._meta import api_version_prefix as VTAG from ..director_v2.exceptions import DirectorServiceError from ..login.decorators import login_required from ..notifications import project_logs from ..products.plugin import Product, get_current_product from ..security.decorators import permission_required +from ..users import api from . import projects_api from .projects_exceptions import ( ProjectInvalidRightsError, @@ -69,9 +69,7 @@ async def open_project(request: web.Request) -> web.Response: project_type: ProjectType = await projects_api.get_project_type( request.app, path_params.project_id ) - user_role: UserRole = await users_api.get_user_role( - request.app, req_ctx.user_id - ) + user_role: UserRole = await api.get_user_role(request.app, req_ctx.user_id) if project_type is ProjectType.TEMPLATE and user_role < UserRole.USER: # only USERS/TESTERS can do that raise web.HTTPForbidden(reason="Wrong user role to open/edit a template") diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py b/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py index df7b3973df9..4f43baa91a7 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_handlers_crud.py @@ -37,7 +37,7 @@ from ..resource_manager.websocket_manager import PROJECT_ID_KEY, managed_resource from ..security.api import check_permission from ..security.decorators import permission_required -from ..users_api import get_user_name +from ..users.api import get_user_name from . import _create_utils, _read_utils, projects_api from ._permalink import update_or_pop_permalink_in_project from ._rest_schemas import ( diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_nodes_handlers.py b/services/web/server/src/simcore_service_webserver/projects/projects_nodes_handlers.py index 87a3ff9a0eb..365853b746a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_nodes_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_nodes_handlers.py @@ -32,7 +32,7 @@ from ..login.decorators import login_required from ..projects.projects_db import ProjectDBAPI from ..security.decorators import permission_required -from ..users_api import get_user_role +from ..users.api import get_user_role from . import projects_api from .projects_exceptions import ( NodeNotFoundError, diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py index 8ee18c6affa..50be0e739e6 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py @@ -21,8 +21,8 @@ from ..login.utils import ACTIVE, GUEST, get_client_ip, get_random_string from ..redis import get_redis_lock_manager_client from ..security.api import authorized_userid, encrypt_password, is_anonymous, remember -from ..users_api import get_user -from ..users_exceptions import UserNotFoundError +from ..users.api import get_user +from ..users.exceptions import UserNotFoundError from ._constants import MSG_GUESTS_NOT_ALLOWED from .settings import StudiesDispatcherSettings, get_plugin_settings diff --git a/services/web/server/src/simcore_service_webserver/users/__init__.py b/services/web/server/src/simcore_service_webserver/users/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/services/web/server/src/simcore_service_webserver/users/_db.py b/services/web/server/src/simcore_service_webserver/users/_db.py new file mode 100644 index 00000000000..5650655c95e --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/users/_db.py @@ -0,0 +1,36 @@ +import sqlalchemy as sa +from aiopg.sa.connection import SAConnection +from aiopg.sa.result import ResultProxy +from models_library.users import GroupID, UserID +from simcore_postgres_database.models.users import UserStatus, users +from sqlalchemy.sql import func + +from ..db_models import user_to_groups + + +async def do_update_expired_users(conn: SAConnection) -> list[UserID]: + + result: ResultProxy = await conn.execute( + users.update() + .values(status=UserStatus.EXPIRED) + .where( + (users.c.expires_at != None) + & (users.c.status == UserStatus.ACTIVE) + & (users.c.expires_at < func.now()) + ) + .returning(users.c.id) + ) + if rows := await result.fetchall(): + expired = [r.id for r in rows] + return expired + return [] + + +async def get_users_ids_in_group(conn: SAConnection, gid: GroupID) -> set[UserID]: + result: set[UserID] = set() + query_result = await conn.execute( + sa.select(user_to_groups.c.uid).where(user_to_groups.c.gid == gid) + ) + async for entry in query_result: + result.add(entry[0]) + return result diff --git a/services/web/server/src/simcore_service_webserver/users_handlers.py b/services/web/server/src/simcore_service_webserver/users/_handlers.py similarity index 74% rename from services/web/server/src/simcore_service_webserver/users_handlers.py rename to services/web/server/src/simcore_service_webserver/users/_handlers.py index 08673cd91c5..9747b269a35 100644 --- a/services/web/server/src/simcore_service_webserver/users_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users/_handlers.py @@ -1,32 +1,30 @@ -# pylint: disable=no-value-for-parameter - -import json -import logging +import functools from typing import Any import redis.asyncio as aioredis from aiohttp import web from models_library.generics import Envelope from pydantic import BaseModel +from servicelib.aiohttp.typing_extension import Handler +from servicelib.json_serialization import json_dumps from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from servicelib.request_keys import RQT_USERID_KEY from servicelib.rest_constants import RESPONSE_MODEL_POLICY -from . import users_api -from ._meta import API_VTAG -from .login.decorators import login_required -from .redis import get_redis_user_notifications_client -from .security.decorators import permission_required -from .user_notifications import ( +from .._meta import API_VTAG +from ..login.decorators import login_required +from ..redis import get_redis_user_notifications_client +from ..security.decorators import permission_required +from ..utils_aiohttp import envelope_json_response +from . import _tokens, api +from ._notifications import ( MAX_NOTIFICATIONS_FOR_USER_TO_KEEP, MAX_NOTIFICATIONS_FOR_USER_TO_SHOW, UserNotification, get_notification_key, ) -from .users_exceptions import TokenNotFoundError, UserNotFoundError -from .users_models import ProfileGet, ProfileUpdate - -logger = logging.getLogger(__name__) +from .exceptions import TokenNotFoundError, UserNotFoundError +from .schemas import ProfileGet, ProfileUpdate # me/ ----------------------------------------------------------- @@ -35,7 +33,7 @@ async def get_my_profile(request: web.Request): # NOTE: ONLY login required to see its profile. E.g. anonymous can never see its profile uid = request[RQT_USERID_KEY] try: - profile: ProfileGet = await users_api.get_user_profile(request.app, uid) + profile: ProfileGet = await api.get_user_profile(request.app, uid) return web.Response( text=Envelope[ProfileGet](data=profile).json(**RESPONSE_MODEL_POLICY), content_type=MIMETYPE_APPLICATION_JSON, @@ -53,46 +51,60 @@ async def update_my_profile(request: web.Request): body = await request.json() updates = ProfileUpdate.parse_obj(body) - await users_api.update_user_profile(request.app, uid, updates) + await api.update_user_profile(request.app, uid, updates) raise web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON) # me/tokens/ ------------------------------------------------------ + + +def _handle_tokens_errors(handler: Handler): + @functools.wraps(handler) + async def _wrapper(request: web.Request) -> web.StreamResponse: + try: + return await handler(request) + + except TokenNotFoundError as exc: + raise web.HTTPNotFound( + reason=f"Token for {exc.service_id} not found" + ) from exc + + return _wrapper + + @login_required @permission_required("user.tokens.*") async def create_tokens(request: web.Request): uid = request[RQT_USERID_KEY] - - # TODO: validate body = await request.json() - # TODO: what it service exists already!? - # TODO: if service already, then IntegrityError is raised! How to deal with db exceptions?? - await users_api.create_token(request.app, uid, body) + await _tokens.create_token(request.app, uid, body) raise web.HTTPCreated( - text=json.dumps({"data": body}), content_type=MIMETYPE_APPLICATION_JSON + text=json_dumps({"data": body}), content_type=MIMETYPE_APPLICATION_JSON ) @login_required @permission_required("user.tokens.*") async def list_tokens(request: web.Request): - # TODO: start = request.match_info.get('start', 0) - # TODO: count = request.match_info.get('count', None) uid = request[RQT_USERID_KEY] - return await users_api.list_tokens(request.app, uid) + all_tokens = await _tokens.list_tokens(request.app, uid) + return envelope_json_response(all_tokens) @login_required +@_handle_tokens_errors @permission_required("user.tokens.*") async def get_token(request: web.Request): uid = request[RQT_USERID_KEY] service_id = request.match_info["service"] - return await users_api.get_token(request.app, uid, service_id) + one_token = await _tokens.get_token(request.app, uid, service_id) + return envelope_json_response(one_token) @login_required +@_handle_tokens_errors @permission_required("user.tokens.*") async def update_token(request: web.Request): """updates token_data of a given user service @@ -101,26 +113,20 @@ async def update_token(request: web.Request): """ uid = request[RQT_USERID_KEY] service_id = request.match_info["service"] - - # TODO: validate - body = await request.json() - - await users_api.update_token(request.app, uid, service_id, body) - + token_data = await request.json() + await _tokens.update_token(request.app, uid, service_id, token_data) raise web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON) @login_required +@_handle_tokens_errors @permission_required("user.tokens.*") async def delete_token(request: web.Request): uid = request[RQT_USERID_KEY] - service_id = request.match_info.get("service") + service_id = request.match_info["service"] - try: - await users_api.delete_token(request.app, uid, service_id) - raise web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON) - except TokenNotFoundError as exc: - raise web.HTTPNotFound(reason=f"Token for {service_id} not found") from exc + await _tokens.delete_token(request.app, uid, service_id) + raise web.HTTPNoContent(content_type=MIMETYPE_APPLICATION_JSON) # me/notifications ----------------------------------------------------------- diff --git a/services/web/server/src/simcore_service_webserver/user_notifications.py b/services/web/server/src/simcore_service_webserver/users/_notifications.py similarity index 100% rename from services/web/server/src/simcore_service_webserver/user_notifications.py rename to services/web/server/src/simcore_service_webserver/users/_notifications.py diff --git a/services/web/server/src/simcore_service_webserver/users/_tokens.py b/services/web/server/src/simcore_service_webserver/users/_tokens.py new file mode 100644 index 00000000000..ac11f00ca1e --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/users/_tokens.py @@ -0,0 +1,90 @@ +""" Private user tokens from external services (e.g. dat-core) + + Implemented as a stand-alone API but currently only exposed to the handlers +""" +import sqlalchemy as sa +from aiohttp import web +from aiopg.sa.result import RowProxy +from models_library.users import UserID +from sqlalchemy import and_, literal_column + +from ..db import get_database_engine +from ..db_models import tokens +from .exceptions import TokenNotFoundError + + +async def create_token( + app: web.Application, user_id: UserID, token_data: dict[str, str] +) -> dict[str, str]: + async with get_database_engine(app).acquire() as conn: + await conn.execute( + tokens.insert().values( + user_id=user_id, + token_service=token_data["service"], + token_data=token_data, + ) + ) + return token_data + + +async def list_tokens(app: web.Application, user_id: UserID) -> list[dict[str, str]]: + user_tokens = [] + async with get_database_engine(app).acquire() as conn: + async for row in conn.execute( + sa.select(tokens.c.token_data).where(tokens.c.user_id == user_id) + ): + user_tokens.append(row["token_data"]) + return user_tokens + + +async def get_token( + app: web.Application, user_id: UserID, service_id: str +) -> dict[str, str]: + async with get_database_engine(app).acquire() as conn: + result = await conn.execute( + sa.select(tokens.c.token_data).where( + and_(tokens.c.user_id == user_id, tokens.c.token_service == service_id) + ) + ) + if row := await result.first(): + return dict(row["token_data"]) + raise TokenNotFoundError(service_id=service_id) + + +async def update_token( + app: web.Application, user_id: UserID, service_id: str, token_data: dict[str, str] +) -> dict[str, str]: + async with get_database_engine(app).acquire() as conn: + result = await conn.execute( + sa.select(tokens.c.token_data, tokens.c.token_id).where( + (tokens.c.user_id == user_id) & (tokens.c.token_service == service_id) + ) + ) + row = await result.first() + if not row: + raise TokenNotFoundError(service_id=service_id) + + data = dict(row["token_data"]) + tid = row["token_id"] + data.update(token_data) + + resp = await conn.execute( + # pylint: disable=no-value-for-parameter + tokens.update() + .where(tokens.c.token_id == tid) + .values(token_data=data) + .returning(literal_column("*")) + ) + assert resp.rowcount == 1 # nosec + updated_token: RowProxy = await resp.fetchone() + return dict(updated_token["token_data"]) + + +async def delete_token(app: web.Application, user_id: UserID, service_id: str) -> None: + async with get_database_engine(app).acquire() as conn: + await conn.execute( + # pylint: disable=no-value-for-parameter + tokens.delete().where( + and_(tokens.c.user_id == user_id, tokens.c.token_service == service_id) + ) + ) diff --git a/services/web/server/src/simcore_service_webserver/users_api.py b/services/web/server/src/simcore_service_webserver/users/api.py similarity index 58% rename from services/web/server/src/simcore_service_webserver/users_api.py rename to services/web/server/src/simcore_service_webserver/users/api.py index 36b9aca4698..d2b47753f07 100644 --- a/services/web/server/src/simcore_service_webserver/users_api.py +++ b/services/web/server/src/simcore_service_webserver/users/api.py @@ -12,20 +12,19 @@ from aiohttp import web from aiopg.sa.engine import Engine from aiopg.sa.result import RowProxy -from models_library.users import UserID -from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY +from models_library.users import GroupID, UserID from simcore_postgres_database.models.users import UserNameConverter, UserRole -from sqlalchemy import and_, literal_column -from .db_models import GroupType, groups, tokens, user_to_groups, users -from .groups.schemas import convert_groups_db_to_schema -from .login.storage import AsyncpgStorage, get_plugin_storage -from .security.api import clean_auth_policy_cache -from .users_exceptions import UserNotFoundError -from .users_models import ProfileGet, ProfileUpdate -from .users_utils import convert_user_db_to_schema +from ..db import get_database_engine +from ..db_models import GroupType, groups, user_to_groups, users +from ..groups.schemas import convert_groups_db_to_schema +from ..login.storage import AsyncpgStorage, get_plugin_storage +from ..security.api import clean_auth_policy_cache +from . import _db +from .exceptions import UserNotFoundError +from .schemas import ProfileGet, ProfileUpdate, convert_user_db_to_schema -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) def _parse_as_user(user_id: Any) -> UserID: @@ -41,38 +40,33 @@ def _parse_as_user(user_id: Any) -> UserID: return user_id -# USERS API ---------------------------------------------------------------------------- - - async def get_user_profile(app: web.Application, user_id: UserID) -> ProfileGet: """ :raises UserNotFoundError: """ - engine: Engine = app[APP_DB_ENGINE_KEY] + engine = get_database_engine(app) user_profile: dict[str, Any] = {} user_primary_group = all_group = {} user_standard_groups = [] user_id = _parse_as_user(user_id) async with engine.acquire() as conn: + row: RowProxy async for row in conn.execute( - sa.select( - [ - users, - groups, - user_to_groups.c.access_rights, - ], - use_labels=True, - ) + sa.select(users, groups, user_to_groups.c.access_rights) .select_from( - users.join( - user_to_groups.join(groups, user_to_groups.c.gid == groups.c.gid), + sa.join( + users, + sa.join( + user_to_groups, groups, user_to_groups.c.gid == groups.c.gid + ), users.c.id == user_to_groups.c.uid, ) ) .where(users.c.id == user_id) .order_by(sa.asc(groups.c.name)) + .apply_labels() ): user_profile.update(convert_user_db_to_schema(row, prefix="users_")) if row["groups_type"] == GroupType.EVERYONE: @@ -111,13 +105,13 @@ async def get_user_profile(app: web.Application, user_id: UserID) -> ProfileGet: async def update_user_profile( - app: web.Application, user_id: int, profile_update: ProfileUpdate + app: web.Application, user_id: UserID, profile_update: ProfileUpdate ) -> None: """ :raises UserNotFoundError: """ - engine = app[APP_DB_ENGINE_KEY] + engine = get_database_engine(app) user_id = _parse_as_user(user_id) async with engine.acquire() as conn: @@ -152,7 +146,7 @@ async def get_user_role(app: web.Application, user_id: UserID) -> UserRole: """ user_id = _parse_as_user(user_id) - engine: Engine = app[APP_DB_ENGINE_KEY] + engine = get_database_engine(app) async with engine.acquire() as conn: user_role: RowProxy | None = await conn.scalar( sa.select(users.c.role).where(users.c.id == user_id) @@ -163,8 +157,8 @@ async def get_user_role(app: web.Application, user_id: UserID) -> UserRole: async def get_guest_user_ids_and_names(app: web.Application) -> list[tuple[int, str]]: - engine: Engine = app[APP_DB_ENGINE_KEY] - result = deque() + engine = get_database_engine(app) + result: deque = deque() async with engine.acquire() as conn: async for row in conn.execute( sa.select(users.c.id, users.c.name).where(users.c.role == UserRole.GUEST) @@ -173,16 +167,16 @@ async def get_guest_user_ids_and_names(app: web.Application) -> list[tuple[int, return list(result) -async def delete_user(app: web.Application, user_id: int) -> None: +async def delete_user_without_projects(app: web.Application, user_id: UserID) -> None: """Deletes a user from the database if the user exists""" - # FIXME: user cannot be deleted without deleting first all ist project + # WARNING: user cannot be deleted without deleting first all ist project # otherwise this function will raise asyncpg.exceptions.ForeignKeyViolationError # Consider "marking" users as deleted and havning a background job that # cleans it up db: AsyncpgStorage = get_plugin_storage(app) user = await db.get_user({"id": user_id}) if not user: - logger.warning( + _logger.warning( "User with id '%s' could not be deleted because it does not exist", user_id ) return @@ -199,11 +193,11 @@ class UserNameDict(TypedDict): last_name: str -async def get_user_name(app: web.Application, user_id: int) -> UserNameDict: +async def get_user_name(app: web.Application, user_id: UserID) -> UserNameDict: """ :raises UserNotFoundError: """ - engine = app[APP_DB_ENGINE_KEY] + engine = get_database_engine(app) user_id = _parse_as_user(user_id) async with engine.acquire() as conn: user_name = await conn.scalar( @@ -219,11 +213,11 @@ async def get_user_name(app: web.Application, user_id: int) -> UserNameDict: ) -async def get_user(app: web.Application, user_id: int) -> dict: +async def get_user(app: web.Application, user_id: UserID) -> dict: """ :raises UserNotFoundError: """ - engine = app[APP_DB_ENGINE_KEY] + engine = get_database_engine(app) user_id = _parse_as_user(user_id) async with engine.acquire() as conn: result = await conn.execute(sa.select(users).where(users.c.id == user_id)) @@ -233,93 +227,21 @@ async def get_user(app: web.Application, user_id: int) -> dict: return dict(row) -async def get_user_id_from_gid(app: web.Application, primary_gid: int) -> int: - engine = app[APP_DB_ENGINE_KEY] +async def get_user_id_from_gid(app: web.Application, primary_gid: int) -> UserID: + engine = get_database_engine(app) async with engine.acquire() as conn: - return await conn.scalar( + user_id: UserID = await conn.scalar( sa.select(users.c.id).where(users.c.primary_gid == primary_gid) ) + return user_id -# TOKEN API ---------------------------------------------------------------------------- - - -async def create_token( - app: web.Application, user_id: int, token_data: dict[str, str] -) -> dict[str, str]: - engine = app[APP_DB_ENGINE_KEY] - async with engine.acquire() as conn: - await conn.execute( - # pylint: disable=no-value-for-parameter - tokens.insert().values( - user_id=user_id, - token_service=token_data["service"], - token_data=token_data, - ) - ) - return token_data - - -async def list_tokens(app: web.Application, user_id: int) -> list[dict[str, str]]: - engine = app[APP_DB_ENGINE_KEY] - user_tokens = [] - async with engine.acquire() as conn: - async for row in conn.execute( - sa.select(tokens.c.token_data).where(tokens.c.user_id == user_id) - ): - user_tokens.append(row["token_data"]) - return user_tokens - - -async def get_token( - app: web.Application, user_id: int, service_id: str -) -> dict[str, str]: - engine = app[APP_DB_ENGINE_KEY] - async with engine.acquire() as conn: - result = await conn.execute( - sa.select(tokens.c.token_data).where( - and_(tokens.c.user_id == user_id, tokens.c.token_service == service_id) - ) - ) - row: RowProxy = await result.first() - return dict(row["token_data"]) - - -async def update_token( - app: web.Application, user_id: int, service_id: str, token_data: dict[str, str] -) -> dict[str, str]: - engine = app[APP_DB_ENGINE_KEY] - # TODO: optimize to a single call? +async def get_users_in_group(app: web.Application, gid: GroupID) -> set[UserID]: + engine = get_database_engine(app) async with engine.acquire() as conn: - result = await conn.execute( - sa.select(tokens.c.token_data, tokens.c.token_id).where( - and_(tokens.c.user_id == user_id, tokens.c.token_service == service_id) - ) - ) - row = await result.first() + return await _db.get_users_ids_in_group(conn, gid) - data = dict(row["token_data"]) - tid = row["token_id"] - data.update(token_data) - resp = await conn.execute( - # pylint: disable=no-value-for-parameter - tokens.update() - .where(tokens.c.token_id == tid) - .values(token_data=data) - .returning(literal_column("*")) - ) - assert resp.rowcount == 1 # nosec - updated_token: RowProxy = await resp.fetchone() - return dict(updated_token["token_data"]) - - -async def delete_token(app: web.Application, user_id: int, service_id: str) -> None: - engine = app[APP_DB_ENGINE_KEY] +async def update_expired_users(engine: Engine) -> list[UserID]: async with engine.acquire() as conn: - await conn.execute( - # pylint: disable=no-value-for-parameter - tokens.delete().where( - and_(tokens.c.user_id == user_id, tokens.c.token_service == service_id) - ) - ) + return await _db.do_update_expired_users(conn) diff --git a/services/web/server/src/simcore_service_webserver/users_exceptions.py b/services/web/server/src/simcore_service_webserver/users/exceptions.py similarity index 84% rename from services/web/server/src/simcore_service_webserver/users_exceptions.py rename to services/web/server/src/simcore_service_webserver/users/exceptions.py index fe04503749d..855d5009f84 100644 --- a/services/web/server/src/simcore_service_webserver/users_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/users/exceptions.py @@ -1,7 +1,5 @@ """Defines the different exceptions that may arise in the projects subpackage""" -from typing import Optional - class UsersException(Exception): """Basic exception for errors raised in projects""" @@ -13,7 +11,7 @@ def __init__(self, msg: str = None): class UserNotFoundError(UsersException): """User in group was not found in DB""" - def __init__(self, *, uid: Optional[int] = None, email: Optional[str] = None): + def __init__(self, *, uid: int | None = None, email: str | None = None): super().__init__( f"User id {uid} not found" if uid else f"User with email {email} not found" ) diff --git a/services/web/server/src/simcore_service_webserver/users.py b/services/web/server/src/simcore_service_webserver/users/plugin.py similarity index 82% rename from services/web/server/src/simcore_service_webserver/users.py rename to services/web/server/src/simcore_service_webserver/users/plugin.py index 735a8286a3c..71c7c5eecbe 100644 --- a/services/web/server/src/simcore_service_webserver/users.py +++ b/services/web/server/src/simcore_service_webserver/users/plugin.py @@ -12,10 +12,10 @@ map_handlers_with_operations, ) -from . import users_handlers -from ._constants import APP_OPENAPI_SPECS_KEY +from .._constants import APP_OPENAPI_SPECS_KEY +from . import _handlers -logger = logging.getLogger(__name__) +_logger = logging.getLogger(__name__) @app_module_setup( @@ -23,7 +23,7 @@ ModuleCategory.ADDON, settings_name="WEBSERVER_USERS", depends=["simcore_service_webserver.rest"], - logger=logger, + logger=_logger, ) def setup_users(app: web.Application): assert app[APP_SETTINGS_KEY].WEBSERVER_USERS # nosec @@ -31,7 +31,7 @@ def setup_users(app: web.Application): # routes related with users specs = app[APP_OPENAPI_SPECS_KEY] routes = map_handlers_with_operations( - get_handlers_from_namespace(users_handlers), + get_handlers_from_namespace(_handlers), filter(lambda o: "me" in o[1].split("/"), iter_path_operations(specs)), strict=True, ) diff --git a/services/web/server/src/simcore_service_webserver/users_models.py b/services/web/server/src/simcore_service_webserver/users/schemas.py similarity index 77% rename from services/web/server/src/simcore_service_webserver/users_models.py rename to services/web/server/src/simcore_service_webserver/users/schemas.py index ceec88bf895..9c2da3307d5 100644 --- a/services/web/server/src/simcore_service_webserver/users_models.py +++ b/services/web/server/src/simcore_service_webserver/users/schemas.py @@ -1,5 +1,5 @@ from datetime import date -from typing import Literal +from typing import Any, Literal, Mapping from uuid import UUID from models_library.basic_types import IdInt @@ -8,7 +8,8 @@ from servicelib.json_serialization import json_dumps from simcore_postgres_database.models.users import UserRole -from .groups.schemas import AllUsersGroups +from ..groups.schemas import AllUsersGroups +from ..utils import gravatar_hash # # TOKENS resource @@ -104,3 +105,28 @@ def to_capitalize(cls, v): if isinstance(v, UserRole): return v.name.capitalize() return v + + +# +# helpers +# + + +def convert_user_db_to_schema( + row: Mapping[str, Any], prefix: Literal["users_", ""] = "" +) -> dict[str, Any]: + # NOTE: this type of functions will be replaced by pydantic. + assert prefix is not None # nosec + parts = row[f"{prefix}name"].split(".") + [""] + data = { + "id": row[f"{prefix}id"], + "login": row[f"{prefix}email"], + "first_name": parts[0], + "last_name": parts[1], + "role": row[f"{prefix}role"].name.capitalize(), + "gravatar_id": gravatar_hash(row[f"{prefix}email"]), + } + + if expires_at := row[f"{prefix}expires_at"]: + data["expires_at"] = expires_at + return data diff --git a/services/web/server/src/simcore_service_webserver/users_db.py b/services/web/server/src/simcore_service_webserver/users_db.py deleted file mode 100644 index eee890e6173..00000000000 --- a/services/web/server/src/simcore_service_webserver/users_db.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging - -from aiopg.sa.engine import Engine -from aiopg.sa.result import ResultProxy -from models_library.basic_types import IdInt -from simcore_postgres_database.models.users import UserStatus, users -from sqlalchemy.sql import func - -logger = logging.getLogger(__name__) - - -async def update_expired_users(engine: Engine) -> list[IdInt]: - async with engine.acquire() as conn: - result: ResultProxy = await conn.execute( - users.update() - .values(status=UserStatus.EXPIRED) - .where( - (users.c.expires_at != None) - & (users.c.status == UserStatus.ACTIVE) - & (users.c.expires_at < func.now()) - ) - .returning(users.c.id) - ) - expired = [r.id for r in await result.fetchall()] - return expired diff --git a/services/web/server/src/simcore_service_webserver/users_to_groups_api.py b/services/web/server/src/simcore_service_webserver/users_to_groups_api.py deleted file mode 100644 index 868122576a0..00000000000 --- a/services/web/server/src/simcore_service_webserver/users_to_groups_api.py +++ /dev/null @@ -1,19 +0,0 @@ -from typing import Set - -import sqlalchemy as sa -from aiohttp import web -from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY - -from .db_models import user_to_groups - - -async def get_users_for_gid(app: web.Application, gid: int) -> Set[int]: - engine = app[APP_DB_ENGINE_KEY] - result = set() - async with engine.acquire() as conn: - query_result = await conn.execute( - sa.select([user_to_groups.c.uid]).where(user_to_groups.c.gid == gid) - ) - async for entry in query_result: - result.add(entry[0]) - return result diff --git a/services/web/server/src/simcore_service_webserver/users_utils.py b/services/web/server/src/simcore_service_webserver/users_utils.py deleted file mode 100644 index 783262d2371..00000000000 --- a/services/web/server/src/simcore_service_webserver/users_utils.py +++ /dev/null @@ -1,24 +0,0 @@ -import logging -from typing import Any, Mapping, Optional - -from .utils import gravatar_hash - -logger = logging.getLogger(__name__) - - -def convert_user_db_to_schema( - row: Mapping[str, Any], prefix: Optional[str] = "" -) -> dict[str, Any]: - parts = row[f"{prefix}name"].split(".") + [""] - data = { - "id": row[f"{prefix}id"], - "login": row[f"{prefix}email"], - "first_name": parts[0], - "last_name": parts[1], - "role": row[f"{prefix}role"].name.capitalize(), - "gravatar_id": gravatar_hash(row[f"{prefix}email"]), - } - - if expires_at := row[f"{prefix}expires_at"]: - data["expires_at"] = expires_at - return data diff --git a/services/web/server/tests/integration/01/test_garbage_collection.py b/services/web/server/tests/integration/01/test_garbage_collection.py index fa731490ef5..2290c024dbc 100644 --- a/services/web/server/tests/integration/01/test_garbage_collection.py +++ b/services/web/server/tests/integration/01/test_garbage_collection.py @@ -44,7 +44,7 @@ from simcore_service_webserver.security.plugin import setup_security from simcore_service_webserver.session import setup_session from simcore_service_webserver.socketio.plugin import setup_socketio -from simcore_service_webserver.users import setup_users +from simcore_service_webserver.users.plugin import setup_users from sqlalchemy import func, select log = logging.getLogger(__name__) diff --git a/services/web/server/tests/integration/02/test_computation.py b/services/web/server/tests/integration/02/test_computation.py index ccef8e2421f..50e60a937d0 100644 --- a/services/web/server/tests/integration/02/test_computation.py +++ b/services/web/server/tests/integration/02/test_computation.py @@ -45,7 +45,7 @@ from simcore_service_webserver.security.plugin import setup_security from simcore_service_webserver.session import setup_session from simcore_service_webserver.socketio.plugin import setup_socketio -from simcore_service_webserver.users import setup_users +from simcore_service_webserver.users.plugin import setup_users from tenacity._asyncio import AsyncRetrying from tenacity.retry import retry_if_exception_type from tenacity.stop import stop_after_delay diff --git a/services/web/server/tests/unit/isolated/test_user_notifications.py b/services/web/server/tests/unit/isolated/test_user_notifications.py index 1ab0fdc0265..3ce00575b8b 100644 --- a/services/web/server/tests/unit/isolated/test_user_notifications.py +++ b/services/web/server/tests/unit/isolated/test_user_notifications.py @@ -4,7 +4,7 @@ import pytest from models_library.users import UserID -from simcore_service_webserver.user_notifications import ( +from simcore_service_webserver.users._notifications import ( NotificationCategory, UserNotification, get_notification_key, diff --git a/services/web/server/tests/unit/isolated/test_users_models.py b/services/web/server/tests/unit/isolated/test_users_models.py index 04e5dbdb492..ad9d4a1e74e 100644 --- a/services/web/server/tests/unit/isolated/test_users_models.py +++ b/services/web/server/tests/unit/isolated/test_users_models.py @@ -9,7 +9,7 @@ from models_library.utils.fastapi_encoders import jsonable_encoder from pydantic import BaseModel from simcore_postgres_database.models.users import UserRole -from simcore_service_webserver.users_models import ProfileGet, Token +from simcore_service_webserver.users.schemas import ProfileGet, Token @pytest.mark.parametrize( diff --git a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py index 7dab330a072..0a4b9c4df1f 100644 --- a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py +++ b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_projects.py @@ -26,7 +26,7 @@ _create_project_with_filepicker_and_service, _create_project_with_service, ) -from simcore_service_webserver.users_api import get_user +from simcore_service_webserver.users.api import get_user FAKE_FILE_VIEWS = list_fake_file_consumers() diff --git a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_studies_access.py b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_studies_access.py index 744cfef8f43..a87eb4fde3c 100644 --- a/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_studies_access.py +++ b/services/web/server/tests/unit/with_dbs/01/studies_dispatcher/test_studies_dispatcher_studies_access.py @@ -31,7 +31,10 @@ from servicelib.common_headers import UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE from simcore_service_webserver.projects.project_models import ProjectDict from simcore_service_webserver.projects.projects_api import submit_delete_project_task -from simcore_service_webserver.users_api import delete_user, get_user_role +from simcore_service_webserver.users.api import ( + delete_user_without_projects, + get_user_role, +) async def _get_user_projects(client) -> list[ProjectDict]: @@ -377,7 +380,7 @@ async def enforce_garbage_collect_guest(uid): ) await delete_task - await delete_user(app, uid) + await delete_user_without_projects(app, uid) return uid user_id = await enforce_garbage_collect_guest(uid=data["id"]) diff --git a/services/web/server/tests/unit/with_dbs/01/test_groups.py b/services/web/server/tests/unit/with_dbs/01/test_groups.py index 35dbd656b0d..11667efc91f 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_groups.py +++ b/services/web/server/tests/unit/with_dbs/01/test_groups.py @@ -42,7 +42,7 @@ from simcore_service_webserver.rest import setup_rest from simcore_service_webserver.security.plugin import setup_security from simcore_service_webserver.session import setup_session -from simcore_service_webserver.users import setup_users +from simcore_service_webserver.users.plugin import setup_users from simcore_service_webserver.utils import gravatar_hash diff --git a/services/web/server/tests/unit/with_dbs/02/test_project_lock.py b/services/web/server/tests/unit/with_dbs/02/test_project_lock.py index 99bb99f4d9c..5c80fccb515 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_project_lock.py +++ b/services/web/server/tests/unit/with_dbs/02/test_project_lock.py @@ -20,7 +20,7 @@ is_project_locked, lock_project, ) -from simcore_service_webserver.users_api import UserNameDict +from simcore_service_webserver.users.api import UserNameDict @pytest.fixture() diff --git a/services/web/server/tests/unit/with_dbs/03/garbage_collector/test_resource_manager.py b/services/web/server/tests/unit/with_dbs/03/garbage_collector/test_resource_manager.py index 5f10423c96c..aae4a45b758 100644 --- a/services/web/server/tests/unit/with_dbs/03/garbage_collector/test_resource_manager.py +++ b/services/web/server/tests/unit/with_dbs/03/garbage_collector/test_resource_manager.py @@ -54,9 +54,9 @@ from simcore_service_webserver.session import setup_session from simcore_service_webserver.socketio.messages import SOCKET_IO_PROJECT_UPDATED_EVENT from simcore_service_webserver.socketio.plugin import setup_socketio -from simcore_service_webserver.users import setup_users -from simcore_service_webserver.users_api import delete_user -from simcore_service_webserver.users_exceptions import UserNotFoundError +from simcore_service_webserver.users.api import delete_user_without_projects +from simcore_service_webserver.users.exceptions import UserNotFoundError +from simcore_service_webserver.users.plugin import setup_users from tenacity._asyncio import AsyncRetrying from tenacity.retry import retry_if_exception_type from tenacity.stop import stop_after_delay @@ -893,7 +893,7 @@ async def test_regression_removing_unexisting_user( ) await delete_task # remove user - await delete_user(app=client.app, user_id=logged_user["id"]) + await delete_user_without_projects(app=client.app, user_id=logged_user["id"]) with pytest.raises(UserNotFoundError): await remove_project_dynamic_services( diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py index 3ec2cc003f0..d277d5453fb 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_registration.py @@ -34,7 +34,7 @@ LoginSettingsForProduct, ) from simcore_service_webserver.login.storage import AsyncpgStorage -from simcore_service_webserver.users_models import ProfileGet +from simcore_service_webserver.users.schemas import ProfileGet @pytest.fixture diff --git a/services/web/server/tests/unit/with_dbs/03/test_project_db.py b/services/web/server/tests/unit/with_dbs/03/test_project_db.py index 9c2db1f0ffc..08f4a786bf7 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_project_db.py +++ b/services/web/server/tests/unit/with_dbs/03/test_project_db.py @@ -47,7 +47,7 @@ NodeNotFoundError, ProjectNotFoundError, ) -from simcore_service_webserver.users_exceptions import UserNotFoundError +from simcore_service_webserver.users.exceptions import UserNotFoundError from simcore_service_webserver.utils import to_datetime from sqlalchemy.engine.result import Row diff --git a/services/web/server/tests/unit/with_dbs/03/test_users.py b/services/web/server/tests/unit/with_dbs/03/test_users.py index 586af5bcd8b..f7e07fd82ec 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users.py @@ -1,4 +1,6 @@ +# pylint: disable=protected-access # pylint: disable=redefined-outer-name +# pylint: disable=too-many-arguments # pylint: disable=unused-argument # pylint: disable=unused-variable @@ -32,6 +34,7 @@ from servicelib.aiohttp.application import create_safe_application from servicelib.rest_constants import RESPONSE_MODEL_POLICY from simcore_postgres_database.models.users import UserRole +from simcore_service_webserver._meta import api_version_prefix as API_VERSION from simcore_service_webserver.application_settings import setup_settings from simcore_service_webserver.db import APP_DB_ENGINE_KEY, setup_db from simcore_service_webserver.groups.plugin import setup_groups @@ -43,18 +46,16 @@ from simcore_service_webserver.rest import setup_rest from simcore_service_webserver.security.plugin import setup_security from simcore_service_webserver.session import setup_session -from simcore_service_webserver.user_notifications import ( +from simcore_service_webserver.users._handlers import _get_user_notifications +from simcore_service_webserver.users._notifications import ( MAX_NOTIFICATIONS_FOR_USER_TO_KEEP, MAX_NOTIFICATIONS_FOR_USER_TO_SHOW, NotificationCategory, UserNotification, get_notification_key, ) -from simcore_service_webserver.users import setup_users -from simcore_service_webserver.users_handlers import _get_user_notifications -from simcore_service_webserver.users_models import ProfileGet - -API_VERSION = "v0" +from simcore_service_webserver.users.plugin import setup_users +from simcore_service_webserver.users.schemas import ProfileGet @pytest.fixture diff --git a/services/web/server/tests/unit/with_dbs/03/test_users_db.py b/services/web/server/tests/unit/with_dbs/03/test_users_db.py index d9becbf600a..4a67658d9f0 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_users_db.py +++ b/services/web/server/tests/unit/with_dbs/03/test_users_db.py @@ -3,7 +3,6 @@ # pylint: disable=unused-variable from datetime import datetime, timedelta -from typing import Optional import pytest from aiohttp import web @@ -15,7 +14,7 @@ from pytest_simcore.helpers.utils_login import NewUser from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY from simcore_postgres_database.models.users import UserStatus -from simcore_service_webserver.users_db import update_expired_users +from simcore_service_webserver.users.api import update_expired_users _NOW = datetime.utcnow() YESTERDAY = _NOW - timedelta(days=1) @@ -34,7 +33,7 @@ def app_environment( @pytest.mark.parametrize("expires_at", (YESTERDAY, TOMORROW, None)) async def test_update_expired_users( - expires_at: Optional[datetime], client: TestClient, faker: Faker + expires_at: datetime | None, client: TestClient, faker: Faker ): has_expired = expires_at == YESTERDAY async with NewUser(