From fd85e3a202967bbcb3d38b3d48eec9cf2e2919f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Wed, 10 May 2023 23:53:35 +0200 Subject: [PATCH] added communication between a token-actor and a token-service --- server/src/uds/REST/methods/actor_v3.py | 120 +++++++++++++----------- server/src/uds/core/services/service.py | 32 ++++--- 2 files changed, 83 insertions(+), 69 deletions(-) diff --git a/server/src/uds/REST/methods/actor_v3.py b/server/src/uds/REST/methods/actor_v3.py index 9a494b317..e5dcdb346 100644 --- a/server/src/uds/REST/methods/actor_v3.py +++ b/server/src/uds/REST/methods/actor_v3.py @@ -33,6 +33,7 @@ import time import logging import typing import functools +import enum from uds.models import ( ActorToken, @@ -76,6 +77,16 @@ class BlockAccess(Exception): pass +class NotifyActionType(enum.StrEnum): + LOGIN = 'login' + LOGOUT = 'logout' + DATA = 'data' + + @staticmethod + def valid_names() -> typing.List[str]: + return [e.value for e in NotifyActionType] + + # Helpers def fixIdsList(idsList: typing.List[str]) -> typing.List[str]: """ @@ -174,6 +185,46 @@ class ActorV3Action(Handler): raise AccessDenied('Access denied') + # Some helpers + def notifyService(self, action: NotifyActionType) -> None: + try: + # If unmanaged, use Service locator + service: 'services.Service' = Service.objects.get(token=self._params['token']).getInstance() + + # We have a valid service, now we can make notifications + + # Build the possible ids and make initial filter to match service + idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10] + + # ensure idsLists has upper and lower versions for case sensitive databases + idsList = fixIdsList(idsList) + + validId: typing.Optional[str] = service.getValidId(idsList) + + is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-') + + # Must be valid + if action in (NotifyActionType.LOGIN, NotifyActionType.LOGOUT): + if not validId: # For login/logout, we need a valid id + raise Exception() + # Notify Service that someone logged in/out + + if action == NotifyActionType.LOGIN: + # Try to guess if this is a remote session + service.processLogin(validId, remote_login=is_remote) + elif action == NotifyActionType.LOGOUT: + service.processLogout(validId, remote_login=is_remote) + elif action == NotifyActionType.DATA: + service.notifyData(validId, self._params['data']) + else: + raise Exception('Invalid action') + + # All right, service notified.. + except Exception as e: + # Log error and continue + logger.error('Error notifying service: %s (%s)', e, self._params) + raise BlockAccess() from None + class Test(ActorV3Action): """ @@ -501,47 +552,7 @@ class Version(ActorV3Action): return ActorV3Action.actorResult() -class LoginLogout(ActorV3Action): - name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available - - def notifyService(self, isLogin: bool) -> None: - try: - # If unmanaged, use Service locator - service: 'services.Service' = Service.objects.get(token=self._params['token']).getInstance() - - # We have a valid service, now we can make notifications - - # Build the possible ids and make initial filter to match service - idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10] - - # ensure idsLists has upper and lower versions for case sensitive databases - idsList = fixIdsList(idsList) - - validId: typing.Optional[str] = service.getValidId(idsList) - - # Must be valid - if not validId: - raise Exception() - - # Recover Id Info from service and validId - # idInfo = service.recoverIdInfo(validId) - - # Notify Service that someone logged in/out - is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-') - if isLogin: - # Try to guess if this is a remote session - service.processLogin(validId, remote_login=is_remote) - else: - service.processLogout(validId, remote_login=is_remote) - - # All right, service notified.. - except Exception as e: - # Log error and continue - logger.error('Error notifying service: %s (%s)', e, self._params) - raise BlockAccess() from None - - -class Login(LoginLogout): +class Login(ActorV3Action): """ Notifies user logged id """ @@ -597,7 +608,7 @@ class Login(LoginLogout): ): # If unamanaged host, lest do a bit more work looking for a service with the provided parameters... if isManaged: raise - self.notifyService(isLogin=True) + self.notifyService(action=NotifyActionType.LOGIN) return ActorV3Action.actorResult( { @@ -610,7 +621,7 @@ class Login(LoginLogout): ) -class Logout(LoginLogout): +class Logout(ActorV3Action): """ Notifies user logged out """ @@ -653,7 +664,7 @@ class Logout(LoginLogout): ): # If unamanaged host, lest do a bit more work looking for a service with the provided parameters... if isManaged: raise - self.notifyService(isLogin=False) # Logout notification + self.notifyService(NotifyActionType.LOGOUT) # Logout notification return ActorV3Action.actorResult( 'notified' ) # Result is that we have not processed the logout in fact, but notified the service @@ -761,7 +772,7 @@ class Unmanaged(ActorV3Action): # Try to infer the ip from the valid id (that could be an IP or a MAC) ip: str try: - ip = next(x['ip'] for x in self._params['id'] if validId in (x['ip'], x['mac'])) + ip = next(x['ip'] for x in self._params['id'] if validId in (x['ip'], x['mac'])) except StopIteration: ip = self._params['id'][0]['ip'] # Get first IP if no valid ip found @@ -802,21 +813,22 @@ class Notify(ActorV3Action): def get(self) -> typing.MutableMapping[str, typing.Any]: logger.debug('Args: %s, Params: %s', self._args, self._params) - if ( - 'action' not in self._params - or 'token' not in self._params - or self._params['action'] not in ('login', 'logout') - ): - # Requested login or logout - raise RequestError('Invalid parameters') + try: + action = NotifyActionType(self._params['action']) + token = self._params['token'] # pylint: disable=unused-variable # Just to check it exists + except Exception as e: + # Requested login, logout or whatever + raise RequestError('Invalid parameters') from e try: # Check block manually checkBlockedIp(self._request) # pylint: disable=protected-access - if self._params['action'] == 'login': + if action == NotifyActionType.LOGIN: Login.action(typing.cast(Login, self)) - else: + elif action == NotifyActionType.LOGOUT: Logout.action(typing.cast(Logout, self)) + elif action == NotifyActionType.DATA: + self.notifyService(action) return ActorV3Action.actorResult('ok') except UserService.DoesNotExist: diff --git a/server/src/uds/core/services/service.py b/server/src/uds/core/services/service.py index b22d42ed3..758fc264f 100644 --- a/server/src/uds/core/services/service.py +++ b/server/src/uds/core/services/service.py @@ -139,9 +139,7 @@ class Service(Module): usesCache = False # : Tooltip to be used if services uses cache at administration interface, indicated by :py:attr:.usesCache - cacheTooltip = _( - 'None' - ) # : Tooltip shown to user when this item is pointed at admin interface + cacheTooltip = _('None') # : Tooltip shown to user when this item is pointed at admin interface # : If user deployments can be cached (see :py:attr:.usesCache), may he also can provide a secondary cache, # : that is no more that user deployments that are "almost ready" to be used, but preperably consumes less @@ -150,9 +148,7 @@ class Service(Module): usesCache_L2 = False # : If we need to generate a "Level 2" cache for this service (i.e., L1 could be running machines and L2 suspended machines) # : Tooltip to be used if services uses L2 cache at administration interface, indicated by :py:attr:.usesCache_L2 - cacheTooltip_L2 = _( - 'None' - ) # : Tooltip shown to user when this item is pointed at admin interface + cacheTooltip_L2 = _('None') # : Tooltip shown to user when this item is pointed at admin interface # : If the service needs a o.s. manager (see os managers section) needsManager: bool = False @@ -270,9 +266,7 @@ class Service(Module): # Keep untouched if maxServices is not present - def requestServicesForAssignation( - self, **kwargs - ) -> typing.Iterable[UserDeployment]: + def requestServicesForAssignation(self, **kwargs) -> typing.Iterable[UserDeployment]: """ override this if mustAssignManualy is True @params kwargs: Named arguments @@ -311,9 +305,7 @@ class Service(Module): return [] def assignFromAssignables( - self, assignableId: str, - user: 'models.User', - userDeployment: UserDeployment + self, assignableId: str, user: 'models.User', userDeployment: UserDeployment ) -> str: """ Assigns from it internal assignable list to an user @@ -384,6 +376,18 @@ class Service(Module): """ return + def notifyData(self, id: typing.Optional[str], data: str) -> None: + """ + Processes a custom data notification, that must be interpreted by the service itself. + This allows "token actors" to communicate with service directly, what is needed for + some kind of services (like LinuxApps) + + Args: + id (typing.Optional[str]): Id validated through "getValidId". May be None if not validated (or not provided) + data (str): Data to process + """ + return + def storeIdInfo(self, id: str, data: typing.Any) -> None: self.storage.putPickle('__nfo_' + id, data) @@ -401,9 +405,7 @@ class Service(Module): from uds.models import Service as DBService # pylint: disable=import-outside-toplevel if self.getUuid(): - log.doLog( - DBService.objects.get(uuid=self.getUuid()), level, message, log.LogSource.SERVICE - ) + log.doLog(DBService.objects.get(uuid=self.getUuid()), level, message, log.LogSource.SERVICE) @classmethod def canAssign(cls) -> bool: