From 74ad50d7d8d9c312cdd4462988bccf181a1f520a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Sun, 8 Nov 2020 19:17:29 +0100 Subject: [PATCH] working on allowing services to detect user login on unmanaged services --- actor/src/udsactor/linux/operations.py | 6 +- actor/src/udsactor/rest.py | 90 ++++++++++++------ actor/src/udsactor/service.py | 47 ++++++++-- server/src/uds/REST/methods/actor_v3.py | 120 +++++++++++++++++++----- server/src/uds/REST/processors.py | 6 +- server/src/uds/core/services/service.py | 22 +++++ 6 files changed, 226 insertions(+), 65 deletions(-) diff --git a/actor/src/udsactor/linux/operations.py b/actor/src/udsactor/linux/operations.py index ba94ba38..4f360d8f 100644 --- a/actor/src/udsactor/linux/operations.py +++ b/actor/src/udsactor/linux/operations.py @@ -186,9 +186,9 @@ def getCurrentUser() -> str: def getSessionType() -> str: ''' Known values: - * Unknown -> No SESSIONNAME environment variable - * Console -> Local session - * RDP-Tcp#[0-9]+ -> RDP Session + * Unknown -> No XDG_SESSION_TYPE environment variable + * xrdp --> xrdp session + * other types ''' return 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown') diff --git a/actor/src/udsactor/rest.py b/actor/src/udsactor/rest.py index 05eefbb3..8ea8c683 100644 --- a/actor/src/udsactor/rest.py +++ b/actor/src/udsactor/rest.py @@ -48,28 +48,37 @@ TIMEOUT = 5 # 5 seconds is more than enought # Constants UNKNOWN = 'unknown' + class RESTError(Exception): ERRCODE = 0 + class RESTConnectionError(RESTError): ERRCODE = -1 # Errors ""raised"" from broker + + class RESTInvalidKeyError(RESTError): ERRCODE = 1 + class RESTUnmanagedHostError(RESTError): ERRCODE = 2 + class RESTUserServiceNotFoundError(RESTError): ERRCODE = 3 + class RESTOsManagerError(RESTError): ERRCODE = 4 # # Basic UDS Api # + + class UDSApi: # pylint: disable=too-few-public-methods """ Base for remote api accesses @@ -101,11 +110,11 @@ class UDSApi: # pylint: disable=too-few-public-methods raise NotImplementedError def _doPost( - self, - method: str, # i.e. 'initialize', 'ready', .... - payLoad: typing.MutableMapping[str, typing.Any], - headers: typing.Optional[typing.MutableMapping[str, str]] = None - ) -> typing.Any: + self, + method: str, # i.e. 'initialize', 'ready', .... + payLoad: typing.MutableMapping[str, typing.Any], + headers: typing.Optional[typing.MutableMapping[str, str]] = None + ) -> typing.Any: headers = headers or self._headers try: result = requests.post(self._apiURL(method), data=json.dumps(payLoad), headers=headers, verify=self._validateCert, timeout=TIMEOUT) @@ -128,6 +137,8 @@ class UDSApi: # pylint: disable=too-few-public-methods # # UDS Broker API access # + + class UDSServerApi(UDSApi): def _apiURL(self, method: str) -> str: return self._url + 'actor/v3/' + method @@ -148,19 +159,19 @@ class UDSServerApi(UDSApi): except Exception: pass - def register( #pylint: disable=too-many-arguments, too-many-locals - self, - auth: str, - username: str, - password: str, - hostname: str, - ip: str, - mac: str, - preCommand: str, - runOnceCommand: str, - postCommand: str, - logLevel: int - ) -> str: + def register( # pylint: disable=too-many-arguments, too-many-locals + self, + auth: str, + username: str, + password: str, + hostname: str, + ip: str, + mac: str, + preCommand: str, + runOnceCommand: str, + postCommand: str, + logLevel: int + ) -> str: """ Raises an exception if could not register, or registers and returns the "authorization token" """ @@ -198,10 +209,10 @@ class UDSServerApi(UDSApi): raise RESTError(result.content.decode()) - def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actorType: typing.Optional[str]) -> types.InitializationResultType: + def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actor_type: typing.Optional[str]) -> types.InitializationResultType: # Generate id list from netork cards payload = { - 'type': actorType or types.MANAGED, + 'type': actor_type or types.MANAGED, 'token': token, 'version': VERSION, 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces] @@ -267,9 +278,16 @@ class UDSServerApi(UDSApi): password=result['password'] ) - - def login(self, own_token: str, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType: - if not own_token: + def login( + self, + actor_type: typing.Optional[str], + token: str, + username: str, + sessionType: str, + interfaces: typing.Iterable[types.InterfaceInfoType], + secret: typing.Optional[str] + ) -> types.LoginResultInfoType: + if not token: return types.LoginResultInfoType( ip='0.0.0.0', hostname=UNKNOWN, @@ -277,9 +295,12 @@ class UDSServerApi(UDSApi): max_idle=None ) payload = { - 'token': own_token, + 'type': actor_type or types.MANAGED, + 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces], + 'token': token, 'username': username, - 'session_type': sessionType or UNKNOWN + 'session_type': sessionType, + 'secret': secret or '', } result = self._doPost('login', payload) return types.LoginResultInfoType( @@ -289,16 +310,25 @@ class UDSServerApi(UDSApi): max_idle=result['max_idle'] ) - def logout(self, own_token: str, username: str) -> None: - if not own_token: + def logout( + self, + actor_type: typing.Optional[str], + token: str, + username: str, + interfaces: typing.Iterable[types.InterfaceInfoType], + secret: typing.Optional[str] + ) -> None: + if not token: return payload = { - 'token': own_token, - 'username': username + 'type': actor_type or types.MANAGED, + 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces], + 'token': token, + 'username': username, + 'secret': secret or '' } self._doPost('logout', payload) - def log(self, own_token: str, level: int, message: str) -> None: if not own_token: return diff --git a/actor/src/udsactor/service.py b/actor/src/udsactor/service.py index a7b744ae..6e68e4d1 100644 --- a/actor/src/udsactor/service.py +++ b/actor/src/udsactor/service.py @@ -273,7 +273,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces)) return False - # Only removes token for managed machines + # Only removes master token for managed machines (will need it on next client execution) master_token = None if self.isManaged() else self._cfg.master_token self._cfg = self._cfg._replace( master_token=master_token, @@ -314,7 +314,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes if self._loggedIn and self._cfg.own_token: self._loggedIn = False try: - self._api.logout(self._cfg.own_token, '') + self._api.logout( + self._cfg.actorType, + self._cfg.own_token, + '', + self._interfaces + ) except Exception as e: logger.error('Error notifying final logout to UDS: %s', e) @@ -405,18 +410,46 @@ class CommonService: # pylint: disable=too-many-instance-attributes def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType: result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None) self._loggedIn = True + + master_token = None + secret = None + # If unmanaged, do initialization now, because we don't know before this + # Also, even if not initialized, get a "login" notification token if not self.isManaged(): self.initialize() - - if self._cfg.own_token: - result = self._api.login(self._cfg.own_token, username, sessionType) + master_token = self._cfg.master_token + secret = self._secret + + # Own token will not be set if UDS did not assigned the initialized VM to an user + # In that case, take master token (if machine is Unamanaged version) + token = self._cfg.own_token or master_token + if token: + result = self._api.login( + self._cfg.actorType, + token, + username, + sessionType or '', + self._interfaces, + secret + ) return result def logout(self, username: str) -> None: self._loggedIn = False - if self._cfg.own_token: - self._api.logout(self._cfg.own_token, username) + + master_token = self._cfg.master_token if self.isManaged() else None + + # Own token will not be set if UDS did not assigned the initialized VM to an user + # In that case, take master token (if machine is Unamanaged version) + token = self._cfg.own_token or master_token + if token: + self._api.logout( + self._cfg.actorType, + token, + username, + self._interfaces + ) self.onLogout(username) diff --git a/server/src/uds/REST/methods/actor_v3.py b/server/src/uds/REST/methods/actor_v3.py index ab54bb79..8a30d96b 100644 --- a/server/src/uds/REST/methods/actor_v3.py +++ b/server/src/uds/REST/methods/actor_v3.py @@ -32,6 +32,7 @@ import secrets import logging import typing +from uds.models import user from uds.models import ( getSqlDatetimeAsUnix, @@ -61,22 +62,27 @@ logger = logging.getLogger(__name__) ALLOWED_FAILS = 5 UNMANAGED = 'unmanaged' # matches the definition of UDS Actors OFC + class BlockAccess(Exception): pass # Helpers -def checkBlockedIp(ip: str)-> None: + + +def checkBlockedIp(ip: str) -> None: cache = Cache('actorv3') fails = cache.get(ip) or 0 if fails > ALLOWED_FAILS: logger.info('Access to actor from %s is blocked for %s seconds since last fail', ip, GlobalConfig.LOGIN_BLOCK.getInt()) raise BlockAccess() + def incFailedIp(ip: str) -> None: cache = Cache('actorv3') fails = (cache.get(ip) or 0) + 1 cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt()) + class ActorV3Action(Handler): authenticated = False # Actor requests are not authenticated normally path = 'actor/v3' @@ -119,6 +125,7 @@ class ActorV3Action(Handler): raise AccessDenied('Access denied') + class Test(ActorV3Action): """ Tests UDS Broker actor connectivity & key @@ -137,6 +144,7 @@ class Test(ActorV3Action): return ActorV3Action.actorResult('ok') + class Register(ActorV3Action): """ Registers an actor @@ -177,6 +185,7 @@ class Register(ActorV3Action): ) return ActorV3Action.actorResult(actorToken.token) + class Initiialize(ActorV3Action): """ Information about machine action. @@ -267,6 +276,7 @@ class Initiialize(ActorV3Action): except (ActorToken.DoesNotExist, Service.DoesNotExist): raise BlockAccess() + class BaseReadyChange(ActorV3Action): """ Records the IP change of actor @@ -309,7 +319,7 @@ class BaseReadyChange(ActorV3Action): osManager.toReady(userService) userServiceManager().notifyReadyFromOsManager(userService, '') - # Generates a certificate and send it to client. + # Generates a certificate and send it to client. privateKey, cert, password = certs.selfSignedCert(self._params['ip']) # Store certificate with userService userService.setProperty('cert', cert) @@ -318,12 +328,14 @@ class BaseReadyChange(ActorV3Action): return ActorV3Action.actorResult({'private_key': privateKey, 'server_certificate': cert, 'password': password}) + class IpChange(BaseReadyChange): """ Processses IP Change. """ name = 'ipchange' + class Ready(BaseReadyChange): """ Notifies the user service is ready @@ -352,6 +364,7 @@ class Ready(BaseReadyChange): return result + class Version(ActorV3Action): """ Notifies the version. @@ -367,26 +380,75 @@ 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 -class Login(ActorV3Action): + def notifyService(self, login: bool): + try: + # If unmanaged, use Service locator + service : 'services.Service' = Service.objects.get(token=self._params['token']).getInstance() + # Locate an userService that belongs to this service and which + # 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] + + validId: typing.Optional[str] = service.getValidId(idsList) + + # Must be valid + if not validId: + raise Exception() + + # Check secret if is stored + storedInfo : typing.Optional[typing.MutableMapping[str, typing.Any]] = service.recoverIdInfo(validId) + # If no secret valid + if not storedInfo or self._params['secret'] != storedInfo['secret']: + raise Exception() + + # Notify Service that someone logged in/out + if login: + # Try to guess if this is a remote session + is_remote = self._params.get('session_type', '')[:3] in ('xrdp', 'RDP-') + service.processLogin(validId, remote_login=is_remote) + else: + service.processLogout(validId) + + # All right, service notified... + except Exception: + raise BlockAccess() + + +class Login(LoginLogout): """ Notifies user logged id """ name = 'login' def action(self) -> typing.MutableMapping[str, typing.Any]: + isManaged = self._params.get('type') != UNMANAGED + ip = hostname = '' + deadLine = maxIdle = None + logger.debug('Login Args: %s, Params: %s', self._args, self._params) - userService = self.getUserService() - osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance() - if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.) - osmanagers.OSManager.loggedIn(userService, self._params.get('username') or '') - maxIdle = osManager.maxIdle() if osManager else None + try: + userService: typing.Optional[UserService] = self.getUserService() + except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters... + if isManaged: + raise + userService = None # Skip later processing userService + self.notifyService(login=True) - logger.debug('Max idle: %s', maxIdle) + if userService: + osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance() + if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.) + osmanagers.OSManager.loggedIn(userService, self._params.get('username') or '') + + maxIdle = osManager.maxIdle() if osManager else None + + logger.debug('Max idle: %s', maxIdle) + + ip, hostname = userService.getConnectionSource() + deadLine = userService.deployed_service.getDeadline() - ip, hostname = userService.getConnectionSource() - deadLine = userService.deployed_service.getDeadline() return ActorV3Action.actorResult({ 'ip': ip, 'hostname': hostname, @@ -394,27 +456,39 @@ class Login(ActorV3Action): 'max_idle': maxIdle }) -class Logout(ActorV3Action): + +class Logout(LoginLogout): """ Notifies user logged out """ name = 'logout' def action(self) -> typing.MutableMapping[str, typing.Any]: + isManaged = self._params.get('type') != UNMANAGED + logger.debug('Args: %s, Params: %s', self._args, self._params) - userService = self.getUserService() - osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance() - if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.) - osmanagers.OSManager.loggedOut(userService, self._params.get('username') or '') - if osManager: - if osManager.isRemovableOnLogout(userService): - logger.debug('Removable on logout: %s', osManager) + try: + userService: typing.Optional[UserService] = self.getUserService() + except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters... + if isManaged: + raise + userService = None # Skip later processing userService + self.notifyService(login=False) # Logout notification + + if userService: + osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance() + if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.) + osmanagers.OSManager.loggedOut(userService, self._params.get('username') or '') + if osManager: + if osManager.isRemovableOnLogout(userService): + logger.debug('Removable on logout: %s', osManager) + userService.remove() + else: userService.remove() - else: - userService.remove() return ActorV3Action.actorResult('ok') + class Log(ActorV3Action): """ Sends a log from the service @@ -429,6 +503,7 @@ class Log(ActorV3Action): return ActorV3Action.actorResult('ok') + class Ticket(ActorV3Action): """ Gets an stored ticket @@ -449,6 +524,7 @@ class Ticket(ActorV3Action): except TicketStore.DoesNotExists: return ActorV3Action.actorResult(error='Invalid ticket') + class Unmanaged(ActorV3Action): name = 'unmanaged' @@ -502,6 +578,7 @@ class Unmanaged(ActorV3Action): return ActorV3Action.actorResult(cert) + class Notify(ActorV3Action): name = 'notify' @@ -529,4 +606,3 @@ class Notify(ActorV3Action): incFailedIp(self._request.ip) # pylint: disable=protected-access raise AccessDenied('Access denied') - diff --git a/server/src/uds/REST/processors.py b/server/src/uds/REST/processors.py index 0bed88cc..edf4f5ed 100644 --- a/server/src/uds/REST/processors.py +++ b/server/src/uds/REST/processors.py @@ -70,11 +70,11 @@ class ContentProcessor: return self._request.GET.copy() - def processParameters(self) -> typing.Any: + def processParameters(self) -> typing.MutableMapping[str, typing.Any]: """ Returns the parameter from the request """ - return '' + return {} def getResponse(self, obj): """ @@ -119,7 +119,7 @@ class MarshallerProcessor(ContentProcessor): """ marshaller: typing.ClassVar[typing.Any] = None - def processParameters(self): + def processParameters(self) -> typing.MutableMapping[str, typing.Any]: try: if self._request.META.get('CONTENT_LENGTH', '0') == '0' or not self._request.body: return self.processGetParameters() diff --git a/server/src/uds/core/services/service.py b/server/src/uds/core/services/service.py index 3608f222..e7ff131b 100644 --- a/server/src/uds/core/services/service.py +++ b/server/src/uds/core/services/service.py @@ -293,6 +293,28 @@ class Service(Module): def getValidId(self, idsList: typing.Iterable[str]) -> typing.Optional[str]: return None + def processLogin(self, id: str, remote_login: bool) -> None: + """ + In the case that a login is invoked directly on an actor controlled machine with + an service token, this method will be called with provided info by uds actor (parameters) + + Args: + id (str): Id validated through "getValidId" + remote_login (bool): if the login seems to be a remote login + """ + return + + def processLogout(self, id: str) -> None: + """ + In the case that a login is invoked directly on an actor controlled machine with + an service token, this method will be called with provided info by uds actor (parameters) + + Args: + id (str): Id validated through "getValidId" + remote_login (bool): if the login seems to be a remote login + """ + return + def storeIdInfo(self, id: str, data: typing.Any) -> None: self.storage.putPickle('__nfo_' + id, data)