diff --git a/server/src/uds/REST/methods/actor.py b/server/src/uds/REST/methods/actor.py index b79962c84..53dcac293 100644 --- a/server/src/uds/REST/methods/actor.py +++ b/server/src/uds/REST/methods/actor.py @@ -237,6 +237,8 @@ class Actor(Handler): return 'ok' raise Exception('Unknown message {} for an user service without os manager'.format(message)) res = osmanager.process(service, message, data, options={'scramble': False}) + if not res: + raise Exception('Old Actors not supported by this os Manager!') except Exception as e: logger.exception("Exception processing from OS Manager") return Actor.result(str(e), ERR_OSMANAGER_ERROR) diff --git a/server/src/uds/REST/methods/actor_v2.py b/server/src/uds/REST/methods/actor_v2.py index 7b54f63a9..46809bb4d 100644 --- a/server/src/uds/REST/methods/actor_v2.py +++ b/server/src/uds/REST/methods/actor_v2.py @@ -34,13 +34,28 @@ import secrets import logging import typing -from uds.models import getSqlDatetimeAsUnix, getSqlDatetime, ActorToken +from uds.models import ( + getSqlDatetimeAsUnix, + getSqlDatetime, + ActorToken, + UserService +) from uds.core import VERSION -from ..handlers import Handler +from uds.core.util.state import State +from uds.core.util.cache import Cache +from uds.core.util.config import GlobalConfig + +from ..handlers import Handler, AccessDenied, RequestError + +# Not imported at runtime, just for type checking +if typing.TYPE_CHECKING: + from uds.core import osmanagers logger = logging.getLogger(__name__) +ALLOWED_FAILS = 5 + def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) -> typing.MutableMapping[str, typing.Any]: result = result or '' res = {'result': result, 'stamp': getSqlDatetimeAsUnix()} @@ -48,6 +63,19 @@ def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) - res['error'] = error return res +def checkBlockedIp(ip: str)-> None: + cache = Cache('actorv2') + 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 Exception() + +def incFailedIp(ip: str) -> None: + cache = Cache('actorv2') + fails = (cache.get(ip) or 0) + 1 + cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt()) + + # Enclosed methods under /actor path class ActorV2(Handler): """ @@ -70,7 +98,7 @@ class ActorV2Action(Handler): path = 'actor/v2' def get(self): - return actorResult('') + return actorResult(VERSION) class ActorV2Register(ActorV2Action): """ @@ -115,15 +143,92 @@ class ActorV2Initiialize(ActorV2Action): Information about machine action. Also returns the id used for the rest of the actions. (Only this one will use actor key) """ - name = 'initiaize' + name = 'initialize' - def post(self): + def get(self) -> typing.MutableMapping[str, typing.Any]: + """ + Processes get requests. Basically checks if this is a "postThoughGet" for OpenGnsys or similar + """ + if self._args[0] == 'PostThoughGet': + self._args = self._args[1:] # Remove first argument + return self.post() + + raise RequestError('Invalid request') + + def post(self) -> typing.MutableMapping[str, typing.Any]: + """ + Initialize method expect a json POST with this fields: + * version: str -> Actor version + * token: str -> Valid Actor Token (if invalid, will return an error) + * id: List[dict] -> List of dictionary containing id and mac: + Will return on field "result" a dictinary with: + * own_token: Optional[str] -> Personal uuid for the service (That, on service, will be used from now onwards). If None, there is no own_token + * unique_id: Optional[str] -> If not None, unique id for the service + * max_idle: Optional[int] -> If not None, max configured Idle for the vm + * os: Optional[dict] -> Data returned by os manager for setting up this service. + On error, will return Empty (None) result, and error field + Example: + { + 'version': '3.0', + 'token': 'asbdasdf', + 'maxIdle': 99999 or None, + 'id': [ + { + 'mac': 'xxxxx', + 'ip': 'vvvvvvvv' + }, ... + ] + } + """ + # First, validate token... logger.debug('Args: %s, Params: %s', self._args, self._params) - return actorResult('ok') + try: + checkBlockedIp(self._request.ip) # Raises an exception if ip is temporarily blocked + ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check + # Valid actor token, now validate access allowed. That is, look for a valid mac from the ones provided. + try: + userService: UserService = next( + iter(UserService.objects.filter( + unique_id__in=[i['mac'] for i in self._params.get('id')[:5]], + state__in=[State.USABLE, State.PREPARING] + )) + ) + except Exception as e: + logger.info('Unmanaged host request: %s, %s', self._params, e) + return actorResult({ + 'own_token': None, + 'max_idle': None, + 'unique_id': None, + 'os': None + }) + + # Managed by UDS, get initialization data from osmanager and return it + # Set last seen actor version + userService.setProperty('actor_version', self._params['version']) + maxIdle = None + osData: typing.MutableMapping[str, typing.Any] = {} + if userService.deployed_service.osmanager: + osManager: 'osmanagers.OSManager' = userService.deployed_service.osmanager.getInstance() + maxIdle = osManager.maxIdle() + logger.debug('Max idle: %s', maxIdle) + osData = osManager.actorData(userService) + + return actorResult({ + 'own_token': userService.uuid, + 'unique_id': userService.unique_id, + 'max_idle': maxIdle, + 'os': osData + }) + except ActorToken.DoesNotExist: + incFailedIp(self._request.ip) # For blocking attacks + except Exception: + pass + + raise AccessDenied('Access denied') class ActorV2Login(ActorV2Action): """ - Information about machine + Notifies user logged out """ name = 'login' @@ -133,7 +238,7 @@ class ActorV2Login(ActorV2Action): class ActorV2Logout(ActorV2Action): """ - Information about machine + Notifies user logged in """ name = 'logout' @@ -143,7 +248,7 @@ class ActorV2Logout(ActorV2Action): class ActorV2Log(ActorV2Action): """ - Information about machine + Sends a log from the service """ name = 'log' @@ -153,7 +258,7 @@ class ActorV2Log(ActorV2Action): class ActorV2IpChange(ActorV2Action): """ - Information about machine + Notifies an IP change """ name = 'ipchange' @@ -163,10 +268,20 @@ class ActorV2IpChange(ActorV2Action): class ActorV2Ready(ActorV2Action): """ - Information about machine + Notifies the service is ready """ name = 'ready' def post(self): logger.debug('Args: %s, Params: %s', self._args, self._params) return actorResult('ok') + +class ActorV2Ticket(ActorV2Action): + """ + Gets an stored ticket + """ + name = 'ticket' + + def post(self): + logger.debug('Args: %s, Params: %s', self._args, self._params) + return actorResult('ok') diff --git a/server/src/uds/core/osmanagers/osmanager.py b/server/src/uds/core/osmanagers/osmanager.py index de2c574a8..4de985537 100644 --- a/server/src/uds/core/osmanagers/osmanager.py +++ b/server/src/uds/core/osmanagers/osmanager.py @@ -95,14 +95,47 @@ class OSManager(Module): @return nothing """ - # These methods must be overriden def process(self, userService: 'UserService', message: str, data: typing.Any, options: typing.Optional[typing.Dict[str, typing.Any]] = None) -> str: """ - This method must be overriden so your so manager can manage requests and responses from agent. @param userService: Service that sends the request (virtual machine or whatever) @param message: message to process (os manager dependent) @param data: Data for this message + + Note: this method is deprecated and will be removed on a future release, when pre 3.0 actors support will be drop + For now, this method will be kept on exising os managers for compatibility with old actors, but is not required for + new os managers (that will only be available on actor 3.0) anymore """ + return '' + + # These methods must be overriden + def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: + """ + This method provides information to actor, so actor can complete os configuration. + Currently exists 3 types of os managers + * rename vm and do NOT ADD to AD + { + 'action': 'rename', + 'name': 'xxxxxx' + } + * rename vm and ADD to AD + { + 'action': 'renameAD', + 'name': 'xxxxxxx', + 'ad': 'domain.xxx' + 'ou': 'ou' # or '' if default ou + 'username': 'userwithaddmachineperms@domain.xxxx' + 'password': 'passwordForTheUserWithPerms', + } + * rename vm, do NOT ADD to AD, and change password for an user + { + 'action': 'rename_and_pw' + 'name': 'xxxxx' + 'username': 'username to change pass' + 'password': 'current password for username to change password' + 'newpassword': 'new password to be set for the username' + } + """ + return {} def checkState(self, userService: 'UserService') -> str: """ @@ -240,6 +273,17 @@ class OSManager(Module): log.useLog('logout', uniqueId, serviceIp, userName, knownUserIP, fullUserName, userService.friendly_name, userService.deployed_service.name) + def loginNotified(self, userService: 'UserService', userName: typing.Optional[str] = None) -> None: + self.loggedIn(userService, userName) + + def logoutNotified(self, userService: 'UserService', userName: typing.Optional[str] = None) -> None: + self.loggedOut(userService, userName) + + def readyNotified(self, userService: 'UserService') -> None: + """ + Invoked by actor v2 whenever a service is set as "ready" + """ + def isPersistent(self) -> bool: """ When a publication if finished, old assigned machines will be removed if this value is True. diff --git a/server/src/uds/models/actor_token.py b/server/src/uds/models/actor_token.py index 8f1c6c69f..5ca54be70 100644 --- a/server/src/uds/models/actor_token.py +++ b/server/src/uds/models/actor_token.py @@ -48,4 +48,4 @@ class ActorToken(models.Model): stamp = models.DateTimeField() # Date creation or validation of this entry def __str__(self): - return ''.format(self.token, self.stamp, self.username, self.ip_from, self.ip) + return ''.format(self.token, self.stamp, self.username, self.hostname, self.ip_from) diff --git a/server/src/uds/models/uuid_model.py b/server/src/uds/models/uuid_model.py index 48e3cdda6..d43db85ba 100644 --- a/server/src/uds/models/uuid_model.py +++ b/server/src/uds/models/uuid_model.py @@ -45,7 +45,7 @@ class UUIDModel(models.Model): """ uuid = models.CharField(max_length=50, default=None, null=True, unique=True) - class Meta: + class Meta: # pylint: disable=too-few-public-methods abstract = True def genUuid(self) -> str: @@ -53,7 +53,7 @@ class UUIDModel(models.Model): # Override default save to add uuid def save(self, force_insert=False, force_update=False, using=None, update_fields=None): - if self.uuid is None or self.uuid == '': + if not self.uuid: self.uuid = self.genUuid() elif self.uuid != self.uuid.lower(): self.uuid = self.uuid.lower() # If we modify uuid elsewhere, ensure that it's stored in lower case diff --git a/server/src/uds/osmanagers/LinuxOsManager/linux_osmanager.py b/server/src/uds/osmanagers/LinuxOsManager/linux_osmanager.py index 8f89ea79b..4c19ee7e5 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/linux_osmanager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/linux_osmanager.py @@ -143,6 +143,28 @@ class LinuxOsManager(osmanagers.OSManager): except Exception: log.doLog(service, log.ERROR, "do not understand {0}".format(data), origin) + # default "ready received" does nothing + def readyReceived(self, userService, data): + pass + + def loginNotified(self, userService, userName=None): + if '\\' not in userName: + self.loggedIn(userService, userName) + + def logoutNotified(self, userService, userName=None): + self.loggedOut(userService, userName) + if self.isRemovableOnLogout(userService): + userService.release() + + def readyNotified(self, userService): + return + + def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: + return { + 'action': 'rename', + 'name': userService.getName() + } + def process(self, userService: 'UserService', message: str, data: typing.Any, options: typing.Optional[typing.Dict[str, typing.Any]] = None) -> str: """ We understand this messages: diff --git a/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py b/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py index 03e12dcb6..8be67d36c 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py @@ -30,6 +30,8 @@ """ @author: Adolfo Gómez, dkmaster at dkmon dot com """ +import random +import string import logging import typing @@ -74,8 +76,6 @@ class LinuxRandomPassManager(LinuxOsManager): return username, password def genPassword(self, service): - import random - import string randomPass = service.recoverValue('linOsRandomPass') if randomPass is None: randomPass = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(16)) @@ -90,6 +90,15 @@ class LinuxRandomPassManager(LinuxOsManager): def infoValue(self, service): return 'rename\r{0}\t{1}\t\t{2}'.format(self.getName(service), self._userAccount, self.genPassword(service)) + def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: + return { + 'action': 'rename_and_pw', + 'name': userService.getName(), + 'username': self._userAccount, + 'password': '', # On linux, user password is not needed so we provide an empty one + 'newpassword': self.genPassword(userService) + } + def marshal(self) -> bytes: """ Serializes the os manager data so we can store it in database diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows.py b/server/src/uds/osmanagers/WindowsOsManager/windows.py index e24070f4e..4ac9f5d73 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows.py @@ -109,9 +109,6 @@ class WindowsOsManager(osmanagers.OSManager): pass def getName(self, userService: 'UserService') -> str: - """ - gets name from deployed - """ return userService.getName() def infoVal(self, userService: 'UserService') -> str: @@ -153,6 +150,24 @@ class WindowsOsManager(osmanagers.OSManager): def readyReceived(self, userService, data): pass + def loginNotified(self, userService, userName=None): + if '\\' not in userName: + self.loggedIn(userService, userName) + + def logoutNotified(self, userService, userName=None): + self.loggedOut(userService, userName) + if self.isRemovableOnLogout(userService): + userService.release() + + def readyNotified(self, userService): + return + + def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: + return { + 'action': 'rename', + 'name': userService.getName() + } + def process(self, userService: 'UserService', message: str, data: typing.Any, options: typing.Optional[typing.Dict[str, typing.Any]] = None) -> str: # pylint: disable=too-many-branches """ We understand this messages: diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py b/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py index c6e759fd9..b52d1d8bc 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py @@ -156,9 +156,10 @@ class WinDomainOsManager(WindowsOsManager): raise ldaputil.LDAPError(_str) - def __getGroup(self, ldapConnection: typing.Any) -> str: + def __getGroup(self, ldapConnection: typing.Any) -> typing.Optional[str]: base = ','.join(['DC=' + i for i in self._domain.split('.')]) group = ldaputil.escape(self._group) + obj: typing.Optional[typing.MutableMapping[str, typing.Any]] try: obj = next(ldaputil.getAsDict(ldapConnection, base, "(&(objectClass=group)(|(cn={0})(sAMAccountName={0})))".format(group), ['dn'], sizeLimit=50)) except StopIteration: @@ -176,6 +177,7 @@ class WinDomainOsManager(WindowsOsManager): base = ','.join(['DC=' + i for i in self._domain.split('.')]) fltr = '(&(objectClass=computer)(sAMAccountName={}$))'.format(ldaputil.escape(machineName)) + obj: typing.Optional[typing.MutableMapping[str, typing.Any]] try: obj = next(ldaputil.getAsDict(ldapConnection, base, fltr, ['dn'], sizeLimit=50)) except StopIteration: @@ -186,7 +188,7 @@ class WinDomainOsManager(WindowsOsManager): return obj['dn'] # Returns the DN - def readyReceived(self, userService: 'UserService', data: str) -> None: + def readyNotified(self, userService: 'UserService') -> None: # No group to add if self._group == '': return @@ -227,7 +229,7 @@ class WinDomainOsManager(WindowsOsManager): log.doLog(userService, log.WARN, error, log.OSMANAGER) logger.error(error) - def release(self, userService: 'UserSrevice') -> None: + def release(self, userService: 'UserService') -> None: super().release(userService) # If no removal requested, just return @@ -266,7 +268,7 @@ class WinDomainOsManager(WindowsOsManager): try: ldapConnection = self.__connectLdap() except ldaputil.LDAPError as e: - return _('Check error: {}').format(self.__getLdapError(e)) + return _('Check error: {}').format(e) except dns.resolver.NXDOMAIN: return _('Could not find server parameters (_ldap._tcp.{0} can\'t be resolved)').format(self._domain) except Exception as e: @@ -276,7 +278,7 @@ class WinDomainOsManager(WindowsOsManager): try: ldapConnection.search_st(self._ou, ldap.SCOPE_BASE) # @UndefinedVariable except ldaputil.LDAPError as e: - return _('Check error: {}').format(self.__getLdapError(e)) + return _('Check error: {}').format(e) # Group if self._group != '': @@ -289,14 +291,13 @@ class WinDomainOsManager(WindowsOsManager): @staticmethod def test(env: 'Environment', data: typing.Dict[str, str]) -> typing.List[typing.Any]: logger.debug('Test invoked') - wd: typing.Optional[WinDomainOsManager] = None try: - wd = WinDomainOsManager(env, data) + wd: WinDomainOsManager = WinDomainOsManager(env, data) logger.debug(wd) try: ldapConnection = wd.__connectLdap() except ldaputil.LDAPError as e: - return [False, _('Could not access AD using LDAP ({0})').format(wd.__getLdapError(e))] + return [False, _('Could not access AD using LDAP ({0})').format(e)] ou = wd._ou if ou == '': @@ -318,6 +319,16 @@ class WinDomainOsManager(WindowsOsManager): return [True, _("All parameters seem to work fine.")] + def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: + return { + 'action': 'rename_and_pw', + 'name': userService.getName(), + 'ad': self._domain, + 'ou': self._ou, + 'username': self._account, + 'password': self._password, + } + def infoVal(self, userService: 'UserService') -> str: return 'domain:{0}\t{1}\t{2}\t{3}\t{4}'.format(self.getName(userService), self._domain, self._ou, self._account, self._password) @@ -354,7 +365,7 @@ class WinDomainOsManager(WindowsOsManager): self._serverHint = values[7] else: self._serverHint = '' - + if values[0] == 'v4': self._ssl = values[8] self._removeOnExit = values[9] @@ -373,5 +384,5 @@ class WinDomainOsManager(WindowsOsManager): dct['grp'] = self._group dct['serverHint'] = self._serverHint dct['ssl'] = self._ssl == 'y' - dct['removeOnExit'] = self._removeOnExit == 'y' + dct['removeOnExit'] = self._removeOnExit == 'y' return dct diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows_random.py b/server/src/uds/osmanagers/WindowsOsManager/windows_random.py index 074303095..6adf4f883 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows_random.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows_random.py @@ -96,6 +96,15 @@ class WinRandomPassManager(WindowsOsManager): log.doLog(userService, log.INFO, "Password set to \"{}\"".format(randomPass), log.OSMANAGER) return randomPass + def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: + return { + 'action': 'rename_and_pw', + 'name': userService.getName(), + 'username': self._userAccount, + 'password': self._password, + 'newpassword': self.genPassword(userService) + } + def infoVal(self, userService: 'UserService') -> str: return 'rename:{0}\t{1}\t{2}\t{3}'.format(self.getName(userService), self._userAccount, self._password, self.genPassword(userService)) diff --git a/server/src/uds/web/util/authentication.py b/server/src/uds/web/util/authentication.py index 131073a37..2e4e9e63d 100644 --- a/server/src/uds/web/util/authentication.py +++ b/server/src/uds/web/util/authentication.py @@ -90,10 +90,10 @@ def checkLogin( # pylint: disable=too-many-branches, too-many-statements cache = Cache('auth') cacheKey = str(authenticator.id) + userName - tries = cache.get(cacheKey) - if tries is None: - tries = 0 - if authenticator.getInstance().blockUserOnLoginFailures is True and tries >= GlobalConfig.MAX_LOGIN_TRIES.getInt(): + tries = cache.get(cacheKey) or 0 + triesByIp = cache.get(request.ip) or 0 + maxTries = GlobalConfig.MAX_LOGIN_TRIES.getInt() + if (authenticator.getInstance().blockUserOnLoginFailures is True and (tries >= maxTries) or triesByIp >= maxTries): authLogLogin(request, authenticator, userName, 'Temporarily blocked') return (None, _('Too many authentication errrors. User temporarily blocked')) @@ -106,8 +106,8 @@ def checkLogin( # pylint: disable=too-many-branches, too-many-statements if user is None: logger.debug("Invalid user %s (access denied)", userName) - tries += 1 - cache.put(cacheKey, tries, GlobalConfig.LOGIN_BLOCK.getInt()) + cache.put(cacheKey, tries+1, GlobalConfig.LOGIN_BLOCK.getInt()) + cache.put(request.ip, triesByIp+1, GlobalConfig.LOGIN_BLOCK.getInt()) authLogLogin(request, authenticator, userName, 'Access denied (user not allowed by UDS)') return (None, _('Access denied'))