From e76df6349d3925f17b853a16c3a71480cf12fbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Wed, 27 Nov 2019 12:09:50 +0100 Subject: [PATCH] Advancing on new UDS Actor --- actor/src/udsactor/rest.py | 15 +- actor/src/udsactor/service.py | 248 ++++++------------ actor/src/udsactor/types.py | 9 +- actor/src/udsactor/utils.py | 72 ----- .../src/uds/auths/InternalDB/authenticator.py | 4 +- server/src/uds/core/osmanagers/osmanager.py | 6 +- .../linux_randompass_osmanager.py | 2 +- .../WindowsOsManager/windows_domain.py | 2 +- .../WindowsOsManager/windows_random.py | 2 +- 9 files changed, 103 insertions(+), 257 deletions(-) delete mode 100644 actor/src/udsactor/utils.py diff --git a/actor/src/udsactor/rest.py b/actor/src/udsactor/rest.py index cf9492cdb..cbebaa0ae 100644 --- a/actor/src/udsactor/rest.py +++ b/actor/src/udsactor/rest.py @@ -190,11 +190,20 @@ class REST: result = requests.post(self.url + 'actor/v2/initialize', data=json.dumps(payload), headers=self._headers, verify=self.validateCert) if result.ok: r = result.json()['result'] + os = r['os'] return types.InitializationResultType( own_token=r['own_token'], unique_id=r['unique_id'], max_idle=r['max_idle'], - os=r['os'] + os=types.ActorOsConfigurationType( + action=os['action'], + name=os['name'], + username=os.get('username'), + password=os.get('password'), + new_password=os.get('new_password'), + ad=os.get('ad'), + ou=os.get('ou') + ) ) except requests.ConnectionError as e: raise RESTConnectionError(str(e)) @@ -206,3 +215,7 @@ class REST: def ready(self, own_token: str, interfaces: typing.Iterable[types.InterfaceInfoType]) -> None: # TODO: implement ready return + + def notifyIpChange(self, own_token: str, ip: str) -> None: + # TODO: implement notifyIpChange + return \ No newline at end of file diff --git a/actor/src/udsactor/service.py b/actor/src/udsactor/service.py index e03c63df8..ce734bfc9 100644 --- a/actor/src/udsactor/service.py +++ b/actor/src/udsactor/service.py @@ -32,19 +32,16 @@ import socket import time -import random import os import subprocess import shlex import stat -import json import typing from . import platform from . import rest from . import types -from .script_thread import ScriptExecutorThread -from .utils import exceptionToMessage +# from .script_thread import ScriptExecutorThread from .log import logger @@ -101,6 +98,36 @@ class CommonService: def setReady(self) -> None: if self._cfg.own_token and self._interfaces: self._api.ready(self._cfg.own_token, self._interfaces) + # Cleans sensible data + if self._cfg.config: + self._cfg._replace(config=self._cfg.config._replace(os=None), data=None) + platform.store.writeConfig(self._cfg) + + def configureMachine(self) -> bool: + # Retry configurations, config in case of error 10 times + counter = 1 + while counter < 10 and self._isAlive: + counter += 1 + try: + if self._cfg.config and self._cfg.config.os: + osData = self._cfg.config.os + if osData.action == 'rename': + self.rename(osData.name, osData.username, osData.password, osData.new_password) + elif osData.action == 'rename_ad': + self.joinDomain(osData.name, osData.ad or '', osData.ou or '', osData.username or '', osData.password or '') + + if self._rebootRequested: + try: + platform.operations.reboot() + except Exception as e: + logger.error('Exception on reboot: {}'.format(e)) + return False # Stops service + break + except Exception as e: + logger.error('Got exception operationg machine: {}'.format(e)) + self.doWait(5000) + + return True def initialize(self) -> bool: if not self._cfg.host: # Not configured @@ -110,203 +137,76 @@ class CommonService: while self._isAlive: if not self._interfaces: self._interfaces = list(platform.operations.getNetworkInfo()) - continue + if not self._interfaces: # Wait a bit for interfaces to get initialized... + self.doWait(5000) + continue try: # If master token is present, initialize and get configuration data if self._cfg.master_token: initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces) if not initResult.own_token: # Not managed - logger.fatal('This host is not managed by UDS Broker (ids: {})'.format(ids)) + logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces)) return False + + self._cfg = self._cfg._replace( + master_token=None, + own_token=initResult.own_token, + config=types.ActorDataConfigurationType( + unique_id=initResult.unique_id, + max_idle=initResult.max_idle, + os=initResult.os + ) + ) + + # On first successfull initialization request, master token will dissapear so it will be no more available (not needed anyway) + platform.store.writeConfig(self._cfg) + break # Initial configuration done.. - except rest.RESTConnectionError: - logger.info('Trying to inititialize connection with broker (last error: {})'.format(exceptionToMessage(e))) + except rest.RESTConnectionError as e: + logger.info('Trying to inititialize connection with broker (last error: {})'.format(e)) self.doWait(5000) # Wait a bit and retry except rest.RESTError as e: # Invalid key? logger.error('Error validating with broker. (Invalid token?): {}'.format(e)) - self._cfg.own_token = initResult.own_token - self._cfg.master_token = None - - # Now try to run the "runonce" element - if self._cfg.runonce_command: - logger.info('Executing runOnce app: {}'.format(runOnce)) - if self.execute(self._cfg.runonce_command, 'RunOnce'): - self._cfg.runonce_command = None - platform.store.writeConfig(self._cfg) - # operations.reboot() - return False - - # Broker connection is initialized, now get information about what to - # do - counter = 0 - while self.isAlive: - try: - logger.debug('Requesting information of what to do now') - info = self.api.information() - data = info.split('\r') - if len(data) != 2: - logger.error('The format of the information message is not correct (got {})'.format(info)) - raise Exception - params = data[1].split('\t') - if data[0] == 'rename': - try: - if len(params) == 1: # Simple rename - logger.debug('Renaming computer to {}'.format(params[0])) - self.rename(params[0]) - # Rename with change password for an user - elif len(params) == 4: - logger.debug('Renaming computer to {}'.format(params)) - self.rename(params[0], params[1], params[2], params[3]) - else: - logger.error('Got invalid parameter for rename operation: {}'.format(params)) - return False - break - except Exception as e: - logger.error('Error at computer renaming stage: {}'.format(e.message)) - return None # Will retry complete broker connection if this point is reached - elif data[0] == 'domain': - if len(params) != 5: - logger.error('Got invalid parameters for domain message: {}'.format(params)) - return False # Stop running service - self.joinDomain(params[0], params[1], params[2], params[3], params[4]) - break - else: - logger.error('Unrecognized action sent from broker: {}'.format(data[0])) - return False # Stop running service - except REST.UserServiceNotFoundError: - logger.error('The host has lost the sync state with broker! (host uuid changed?)') - return False - except Exception as err: - if counter % 60 == 0: - logger.warn('Too many retries in progress, though still trying (last error: {})'.format(exceptionToMessage(err))) - counter += 1 - # Any other error is expectable and recoverable, so let's wait - # a bit and retry again - # Wait a bit before next check - self.doWait(5000) - - if self.rebootRequested: - try: - operations.reboot() - except Exception as e: - logger.error('Exception on reboot: {}'.format(e.message)) - return False # Stops service + self.configureMachine() return True def checkIpsChanged(self): - if self.api is None or self.api.uuid is None: - return # Not connected - netInfo = tuple(operations.getNetworkInfo()) - for i in netInfo: - # If at least one ip has changed - if i.mac in self.knownIps and self.knownIps[i.mac] != i.ip: - logger.info('Notifying ip change to broker (mac {}, from {} to {})'.format(i.mac, self.knownIps[i.mac], i.ip)) - try: - # Notifies all interfaces IPs - self.api.notifyIpChanges(((v.mac, v.ip) for v in netInfo)) - - # Regenerates Known ips - self.knownIps = dict(((v.mac, v.ip) for v in netInfo)) - - # And notify new listening address to broker - address = (self.knownIps[self.api.mac], self.httpServer.getPort()) - # And new listening address - self.httpServer.restart(address) - # sends notification - self.api.notifyComm(self.httpServer.getServerUrl()) - - except Exception as e: - logger.warn('Got an error notifiying IPs to broker: {} (will retry in a bit)'.format(e.message.decode('windows-1250', 'ignore'))) - - def clientMessageProcessor(self, msg, data): - logger.debug('Got message {}'.format(msg)) - if self.api is None: - logger.info('Rest api not ready') + if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id: + # Not enouth data do check return - if msg == ipc.REQ_LOGIN: - self.loggedIn = True - res = self.api.login(data).split('\t') - # third parameter, if exists, sets maxSession duration to this. - # First & second parameters are ip & hostname of connection source - if len(res) >= 3: - self.api.maxSession = int(res[2]) # Third parameter is max session duration - msg = ipc.REQ_INFORMATION # Senf information, requested or not, to client on login notification - if msg == ipc.REQ_LOGOUT and self.loggedIn is True: - self.loggedIn = False - self.api.logout(data) - self.onLogout(data) - if msg == ipc.REQ_INFORMATION: - info = {} - if self.api.idle is not None: - info['idle'] = self.api.idle - if self.api.maxSession is not None: - info['maxSession'] = self.api.maxSession - self.ipc.sendInformationMessage(info) - if msg == ipc.REQ_TICKET: - d = json.loads(data) + def locateMac(interfaces: typing.Iterable[types.InterfaceInfoType]) -> typing.Optional[types.InterfaceInfoType]: try: - result = self.api.getTicket(d['ticketId'], d['secure']) - self.ipc.sendTicketMessage(result) - except Exception: - logger.exception('Getting ticket') - self.ipc.sendTicketMessage({'error': 'invalid ticket'}) + return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id.lower()) + except StopIteration: + return None - def initIPC(self): - # ****************************************** - # * Initialize listener IPC & REST threads * - # ****************************************** - logger.debug('Starting IPC listener at {}'.format(IPC_PORT)) - self.ipc = ipc.ServerIPC(IPC_PORT, clientMessageProcessor=self.clientMessageProcessor) - self.ipc.start() - - if self.api.mac in self.knownIps: - address = (self.knownIps[self.api.mac], random.randrange(43900, 44000)) - logger.info('Starting REST listener at {}'.format(address)) - self.httpServer = httpserver.HTTPServerThread(address, self) - self.httpServer.start() - # And notify it to broker - self.api.notifyComm(self.httpServer.getServerUrl()) - - def endIPC(self): - # Remove IPC threads - if self.ipc is not None: - try: - self.ipc.stop() - except Exception: - logger.error('Couln\'t stop ipc server') - if self.httpServer is not None: - try: - self.httpServer.stop() - except Exception: - logger.error('Couln\'t stop REST server') - - def endAPI(self): - if self.api is not None: - try: - if self.loggedIn: - self.loggedIn = False - self.api.logout('service_stopped') - self.api.notifyComm(None) - except Exception as e: - logger.error('Couln\'t remove comms url from broker: {}'.format(e)) - - # self.notifyStop() + try: + oldIp = locateMac(self._interfaces) + newIp = locateMac(platform.operations.getNetworkInfo()) + if not newIp: + raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id)) + if oldIp != newIp: + self._api.notifyIpChange(self._cfg.own_token, newIp) + logger.info('Ip changed from {} to {}. Notified to UDS'.format(oldIp, newIp)) + except Exception as e: + # No ip changed, log exception for info + logger.warn('Checking ips faield: {}'.format(e)) # *************************************************** # Methods that ARE overriden by linux & windows Actor # *************************************************** - def rename(self, name, user=None, oldPassword=None, newPassword=None): + def rename(self, name: str, user: typing.Optional[str] = None, oldPassword: typing.Optional[str] = None, newPassword: typing.Optional[str] = None): ''' Invoked when broker requests a rename action MUST BE OVERRIDEN ''' raise NotImplementedError('Method renamed has not been implemented!') - def joinDomain(self, name, domain, ou, account, password): + def joinDomain(self, name: str, domain: str, ou: str, account: str, password: str): ''' Invoked when broker requests a "domain" action MUST BE OVERRIDEN @@ -317,7 +217,7 @@ class CommonService: # Methods that CAN BE overriden by actors # **************************************** def notifyLocal(self): - self.setReady(operations.getComputerName()) + self.setReady() def doWait(self, miliseconds): ''' @@ -332,12 +232,12 @@ class CommonService: ''' logger.info('Service is being stopped') - def preConnect(self, user, protocol): + def preConnect(self, user: str, protocol: str): ''' Invoked when received a PRE Connection request via REST ''' logger.debug('Pre-connect does nothing') return 'ok' - def onLogout(self, user): + def onLogout(self, user: str): logger.debug('On logout invoked for {}'.format(user)) diff --git a/actor/src/udsactor/types.py b/actor/src/udsactor/types.py index b3106fc97..b8e864d17 100644 --- a/actor/src/udsactor/types.py +++ b/actor/src/udsactor/types.py @@ -2,8 +2,8 @@ import typing class InterfaceInfoType(typing.NamedTuple): name: str - mac: typing.Optional[str] - ip: typing.Optional[str] + mac: str + ip: str class AuthenticatorType(typing.NamedTuple): authId: str @@ -43,5 +43,8 @@ class ActorConfigurationType(typing.NamedTuple): data: typing.Optional[typing.Dict[str, typing.Any]] = None -class InitializationResultType(ActorDataConfigurationType): +class InitializationResultType(typing.NamedTuple): own_token: typing.Optional[str] = None + unique_id: typing.Optional[str] = None + max_idle: typing.Optional[int] = None + os: typing.Optional[ActorOsConfigurationType] = None diff --git a/actor/src/udsactor/utils.py b/actor/src/udsactor/utils.py deleted file mode 100644 index 9480a6ab3..000000000 --- a/actor/src/udsactor/utils.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2014 Virtual Cable S.L. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -''' -@author: Adolfo Gómez, dkmaster at dkmon dot com -''' -from __future__ import unicode_literals - -import sys -import six - -if sys.platform == 'win32': - _fromEncoding = 'windows-1250' -else: - _fromEncoding = 'utf-8' - - -def toUnicode(msg): - try: - if not isinstance(msg, six.text_type): - if isinstance(msg, six.binary_type): - return msg.decode(_fromEncoding, 'ignore') - return six.text_type(msg) - else: - return msg - except Exception: - try: - return six.text_type(msg) - except Exception: - return '' - - -def exceptionToMessage(e): - msg = '' - for arg in e.args: - if isinstance(arg, Exception): - msg = msg + exceptionToMessage(arg) - else: - msg = msg + toUnicode(arg) + '. ' - return msg - - -class Bunch(dict): - def __init__(self, **kw): - dict.__init__(self, kw) - self.__dict__ = self - diff --git a/server/src/uds/auths/InternalDB/authenticator.py b/server/src/uds/auths/InternalDB/authenticator.py index f9ea6205a..297071b4f 100644 --- a/server/src/uds/auths/InternalDB/authenticator.py +++ b/server/src/uds/auths/InternalDB/authenticator.py @@ -35,7 +35,7 @@ import logging import typing -import dns +import dns.resolver from django.utils.translation import ugettext_noop as _ from uds.core import auths @@ -126,6 +126,8 @@ class InternalDBAuth(auths.Authenticator): groupsManager.validate([g.name for g in user.groups.all()]) + def createUser(self, usrData): + pass @staticmethod def test(env, data): diff --git a/server/src/uds/core/osmanagers/osmanager.py b/server/src/uds/core/osmanagers/osmanager.py index 4de985537..ebb8ed04f 100644 --- a/server/src/uds/core/osmanagers/osmanager.py +++ b/server/src/uds/core/osmanagers/osmanager.py @@ -111,7 +111,7 @@ class OSManager(Module): 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 + Currently exists 3 types of os managers actions * rename vm and do NOT ADD to AD { 'action': 'rename', @@ -119,7 +119,7 @@ class OSManager(Module): } * rename vm and ADD to AD { - 'action': 'renameAD', + 'action': 'rename_ad', 'name': 'xxxxxxx', 'ad': 'domain.xxx' 'ou': 'ou' # or '' if default ou @@ -128,7 +128,7 @@ class OSManager(Module): } * rename vm, do NOT ADD to AD, and change password for an user { - 'action': 'rename_and_pw' + 'action': 'rename' 'name': 'xxxxx' 'username': 'username to change pass' 'password': 'current password for username to change password' diff --git a/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py b/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py index 8be67d36c..c087438b0 100644 --- a/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py +++ b/server/src/uds/osmanagers/LinuxOsManager/linux_randompass_osmanager.py @@ -92,7 +92,7 @@ class LinuxRandomPassManager(LinuxOsManager): def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: return { - 'action': 'rename_and_pw', + 'action': 'rename', 'name': userService.getName(), 'username': self._userAccount, 'password': '', # On linux, user password is not needed so we provide an empty one diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py b/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py index b52d1d8bc..5a6919b1e 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows_domain.py @@ -321,7 +321,7 @@ class WinDomainOsManager(WindowsOsManager): def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: return { - 'action': 'rename_and_pw', + 'action': 'rename_ad', 'name': userService.getName(), 'ad': self._domain, 'ou': self._ou, diff --git a/server/src/uds/osmanagers/WindowsOsManager/windows_random.py b/server/src/uds/osmanagers/WindowsOsManager/windows_random.py index 6adf4f883..eb93b58a4 100644 --- a/server/src/uds/osmanagers/WindowsOsManager/windows_random.py +++ b/server/src/uds/osmanagers/WindowsOsManager/windows_random.py @@ -98,7 +98,7 @@ class WinRandomPassManager(WindowsOsManager): def actorData(self, userService: 'UserService') -> typing.MutableMapping[str, typing.Any]: return { - 'action': 'rename_and_pw', + 'action': 'rename', 'name': userService.getName(), 'username': self._userAccount, 'password': self._password,