From e9f38b044f31c2f8ef52cdba43a5eae9de627b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Mon, 8 Dec 2014 12:54:25 +0100 Subject: [PATCH] Now correctly adds user to Remote Desktop Group before connection, so access is granted for user (Note: user must exist in machine/domain) --- actors/src/udsactor/httpserver.py | 6 +- actors/src/udsactor/service.py | 2 +- .../src/udsactor/windows/UDSActorService.py | 4 +- .../uds/core/managers/UserServiceManager.py | 20 +++ .../src/uds/core/transports/BaseTransport.py | 10 +- .../migrations/0009_TransportsToNewerModel.py | 2 +- .../uds/transports/RDP/BaseRDPTransport.py | 124 ++++++++++++++++++ server/src/uds/transports/RDP/RDPTransport.py | 81 ++---------- .../src/uds/transports/RDP/TSRDPTransport.py | 73 +++-------- server/src/uds/web/views.py | 5 +- 10 files changed, 188 insertions(+), 139 deletions(-) create mode 100644 server/src/uds/transports/RDP/BaseRDPTransport.py diff --git a/actors/src/udsactor/httpserver.py b/actors/src/udsactor/httpserver.py index aaffe2ee..16ee7f3f 100644 --- a/actors/src/udsactor/httpserver.py +++ b/actors/src/udsactor/httpserver.py @@ -162,11 +162,11 @@ class HTTPServerHandler(BaseHTTPServer.BaseHTTPRequestHandler): th.start() return 'ok' - def get_preConnect(self, params): + def post_preConnect(self, params): logger.debug('Received Pre connection') - if 'user' not in params: + if 'user' not in params or 'protocol' not in params: raise Exception('Invalid preConnect parameters') - return HTTPServerHandler.service.preConnect(params.get('user')) + return HTTPServerHandler.service.preConnect(params.get('user'), params.get('protocol')) def get_information(self, params): # TODO: Return something useful? :) diff --git a/actors/src/udsactor/service.py b/actors/src/udsactor/service.py index 151c7f95..a7a3376e 100644 --- a/actors/src/udsactor/service.py +++ b/actors/src/udsactor/service.py @@ -296,7 +296,7 @@ class CommonService(object): ''' logger.info('Service is being stopped') - def preConnect(self, user): + def preConnect(self, user, protocol): ''' Invoked when received a PRE Connection request via REST ''' diff --git a/actors/src/udsactor/windows/UDSActorService.py b/actors/src/udsactor/windows/UDSActorService.py index de4f78fc..b06ec9d6 100644 --- a/actors/src/udsactor/windows/UDSActorService.py +++ b/actors/src/udsactor/windows/UDSActorService.py @@ -163,7 +163,9 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): else: self.multiStepJoin(name, domain, ou, account, password) - def preConnect(self, user): + def preConnect(self, user, protocol): + if protocol != 'rdp': # If connection is not using rdp, skip adding user + return 'ok' # Well known SSID for Remote Desktop Users REMOTE_USERS_SID = 'S-1-5-32-555' diff --git a/server/src/uds/core/managers/UserServiceManager.py b/server/src/uds/core/managers/UserServiceManager.py index bf2c9d75..76cfd5b3 100644 --- a/server/src/uds/core/managers/UserServiceManager.py +++ b/server/src/uds/core/managers/UserServiceManager.py @@ -428,6 +428,7 @@ class UserServiceManager(object): return True def isReady(self, uService): + return True UserService.objects.update() uService = UserService.objects.get(id=uService.id) logger.debug('Checking ready of {0}'.format(uService)) @@ -446,6 +447,25 @@ class UserServiceManager(object): UserServiceOpChecker.makeUnique(uService, ui, state) return False + def notifyPreconnect(self, uService, userName, protocol): + url = uService.getCommsUrl() + if url is None: + logger.debug('No notification is made because agent does not supports notifications') + return + url += '/preConnect' + + try: + r = requests.post(url, + data=json.dumps({'user': userName, 'protocol': protocol}), + headers={'content-type': 'application/json'}, + verify=False, + timeout=5) + r = json.loads(r.content) + logger.debug('Sent pre connection to client using {}: {}'.format(url, r)) + # In fact we ignore result right now + except Exception as e: + logger.error('Exception caught notifiying preConnection: {}. Check connection on destination machine: {}'.format(e, url)) + def sendScript(self, uService, script): ''' If allowed, send script to user service diff --git a/server/src/uds/core/transports/BaseTransport.py b/server/src/uds/core/transports/BaseTransport.py index 257ee1b9..67ecd235 100644 --- a/server/src/uds/core/transports/BaseTransport.py +++ b/server/src/uds/core/transports/BaseTransport.py @@ -144,13 +144,13 @@ class Transport(Module): ''' return {'protocol': self.protocol, 'username': '', 'password': '', 'domain': ''} - def preAccessScript(self, userService, user): + def processedUser(self, userService, userName): ''' - This gives us the chance to include "customized" initialization for any transport for an specifyc user & service on assignation to an user - such as "include" in allowed user list, etc... - Both values are db objects + Used to "transform" username that will be sent to service + This is used to make the "user" that will receive the service match with that sent in notification + @return: transformed username ''' - return None + return userName def renderForHtml(self, userService, transport, ip, os, user, password): ''' diff --git a/server/src/uds/migrations/0009_TransportsToNewerModel.py b/server/src/uds/migrations/0009_TransportsToNewerModel.py index 7398c07c..7d5e8f71 100644 --- a/server/src/uds/migrations/0009_TransportsToNewerModel.py +++ b/server/src/uds/migrations/0009_TransportsToNewerModel.py @@ -168,7 +168,7 @@ def unmarshalTRGS(data): def transformTransports(apps, schema_editor): ''' - Adds uuids values to migrated models + Move serialization to a better model (it's time, the mode is there since 1.1 :) ) ''' model = apps.get_model("uds", 'Transport') for t in model.objects.all(): diff --git a/server/src/uds/transports/RDP/BaseRDPTransport.py b/server/src/uds/transports/RDP/BaseRDPTransport.py new file mode 100644 index 00000000..1de03ec9 --- /dev/null +++ b/server/src/uds/transports/RDP/BaseRDPTransport.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- + +# +# Copyright (c) 2012 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 django.utils.translation import ugettext_noop as _ +from uds.core.managers.UserPrefsManager import CommonPrefs +from uds.core.ui.UserInterface import gui +from uds.core.transports.BaseTransport import Transport +from uds.core.transports import protocols +from uds.core.util import connection +from .web import generateHtmlForRdp, getHtmlComponent + +import logging + +logger = logging.getLogger(__name__) + +READY_CACHE_TIMEOUT = 30 + + +class BaseRDPTransport(Transport): + ''' + Provides access via RDP to service. + This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password + ''' + iconFile = 'rdp.png' + needsJava = True # If this transport needs java for rendering + protocol = protocols.RDP + + useEmptyCreds = gui.CheckBoxField(label=_('Empty creds'), order=1, tooltip=_('If checked, the credentials used to connect will be emtpy')) + fixedName = gui.TextField(label=_('Username'), order=2, tooltip=_('If not empty, this username will be always used as credential')) + fixedPassword = gui.PasswordField(label=_('Password'), order=3, tooltip=_('If not empty, this password will be always used as credential')) + withoutDomain = gui.CheckBoxField(label=_('Without Domain'), order=4, tooltip=_('If checked, the domain part will always be emptied (to connecto to xrdp for example is needed)')) + fixedDomain = gui.TextField(label=_('Domain'), order=5, tooltip=_('If not empty, this domain will be always used as credential (used as DOMAIN\\user)')) + allowSmartcards = gui.CheckBoxField(label=_('Allow Smartcards'), order=6, tooltip=_('If checked, this transport will allow the use of smartcards')) + allowPrinters = gui.CheckBoxField(label=_('Allow Printers'), order=7, tooltip=_('If checked, this transport will allow the use of user printers')) + allowDrives = gui.CheckBoxField(label=_('Allow Drives'), order=8, tooltip=_('If checked, this transport will allow the use of user drives')) + allowSerials = gui.CheckBoxField(label=_('Allow Serials'), order=9, tooltip=_('If checked, this transport will allow the use of user serial ports')) + wallpaper = gui.CheckBoxField(label=_('Show wallpaper'), order=10, tooltip=_('If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)')) + + def isAvailableFor(self, ip): + ''' + Checks if the transport is available for the requested destination ip + Override this in yours transports + ''' + logger.debug('Checking availability for {0}'.format(ip)) + ready = self.cache().get(ip) + if ready is None: + # Check again for ready + if connection.testServer(ip, '3389') is True: + self.cache().put(ip, 'Y', READY_CACHE_TIMEOUT) + return True + else: + self.cache().put(ip, 'N', READY_CACHE_TIMEOUT) + return ready == 'Y' + + def processedUser(self, userService, userName): + v = self.processUserPassword(userService, userName, '') + return v['username'] + + def processUserPassword(self, service, user, password): + username = user.getUsernameForAuth() + + if self.fixedName.value is not '': + username = self.fixedName.value + + proc = username.split('@') + if len(proc) > 1: + domain = proc[1] + else: + domain = '' + username = proc[0] + + if self.fixedPassword.value is not '': + password = self.fixedPassword.value + if self.fixedDomain.value is not '': + domain = self.fixedDomain.value + if self.useEmptyCreds.isTrue(): + username, password, domain = '', '', '' + + if self.withoutDomain.isTrue(): + domain = '' + + if '.' in domain: # Dotter domain form + username = username + '@' + domain + domain = '' + + # Fix username/password acording to os manager + username, password = service.processUserPassword(username, password) + + return {'protocol': self.protocol, 'username': username, 'password': password, 'domain': domain} + + def getHtmlComponent(self, _id, _os, componentId): + logger.debug('Component: ID={}'.format(id)) + # We use helper to keep this clean + return getHtmlComponent(self.__module__, componentId) diff --git a/server/src/uds/transports/RDP/RDPTransport.py b/server/src/uds/transports/RDP/RDPTransport.py index 4fa69ba7..62f6eef5 100644 --- a/server/src/uds/transports/RDP/RDPTransport.py +++ b/server/src/uds/transports/RDP/RDPTransport.py @@ -38,6 +38,7 @@ from uds.core.transports.BaseTransport import Transport from uds.core.transports import protocols from uds.core.util import connection from .web import generateHtmlForRdp, getHtmlComponent +from .BaseRDPTransport import BaseRDPTransport import logging @@ -46,7 +47,7 @@ logger = logging.getLogger(__name__) READY_CACHE_TIMEOUT = 30 -class RDPTransport(Transport): +class RDPTransport(BaseRDPTransport): ''' Provides access via RDP to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password @@ -54,71 +55,20 @@ class RDPTransport(Transport): typeName = _('RDP Transport (direct)') typeType = 'RDPTransport' typeDescription = _('RDP Transport for direct connection') - iconFile = 'rdp.png' - needsJava = True # If this transport needs java for rendering - protocol = protocols.RDP - useEmptyCreds = gui.CheckBoxField(label=_('Empty creds'), order=1, tooltip=_('If checked, the credentials used to connect will be emtpy')) - fixedName = gui.TextField(label=_('Username'), order=2, tooltip=_('If not empty, this username will be always used as credential')) - fixedPassword = gui.PasswordField(label=_('Password'), order=3, tooltip=_('If not empty, this password will be always used as credential')) - withoutDomain = gui.CheckBoxField(label=_('Without Domain'), order=4, tooltip=_('If checked, the domain part will always be emptied (to connecto to xrdp for example is needed)')) - fixedDomain = gui.TextField(label=_('Domain'), order=5, tooltip=_('If not empty, this domain will be always used as credential (used as DOMAIN\\user)')) - allowSmartcards = gui.CheckBoxField(label=_('Allow Smartcards'), order=6, tooltip=_('If checked, this transport will allow the use of smartcards')) - allowPrinters = gui.CheckBoxField(label=_('Allow Printers'), order=7, tooltip=_('If checked, this transport will allow the use of user printers')) - allowDrives = gui.CheckBoxField(label=_('Allow Drives'), order=8, tooltip=_('If checked, this transport will allow the use of user drives')) - allowSerials = gui.CheckBoxField(label=_('Allow Serials'), order=9, tooltip=_('If checked, this transport will allow the use of user serial ports')) - wallpaper = gui.CheckBoxField(label=_('Show wallpaper'), order=10, tooltip=_('If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)')) - - def initialize(self, values): - return - - def isAvailableFor(self, ip): - ''' - Checks if the transport is available for the requested destination ip - Override this in yours transports - ''' - logger.debug('Checking availability for {0}'.format(ip)) - ready = self.cache().get(ip) - if ready is None: - # Check again for ready - if connection.testServer(ip, '3389') == True: - self.cache().put(ip, 'Y', READY_CACHE_TIMEOUT) - return True - else: - self.cache().put(ip, 'N', READY_CACHE_TIMEOUT) - return ready == 'Y' + useEmptyCreds = BaseRDPTransport.useEmptyCreds + fixedName = BaseRDPTransport.fixedName + fixedPassword = BaseRDPTransport.fixedPassword + withoutDomain = BaseRDPTransport.withoutDomain + fixedDomain = BaseRDPTransport.fixedDomain + allowSmartcards = BaseRDPTransport.allowSmartcards + allowPrinters = BaseRDPTransport.allowPrinters + allowDrives = BaseRDPTransport.allowDrives + allowSerials = BaseRDPTransport.allowSerials + wallpaper = BaseRDPTransport.wallpaper def getConnectionInfo(self, service, user, password): - username = user.getUsernameForAuth() - - if self.fixedName.value is not '': - username = self.fixedName.value - - proc = username.split('@') - if len(proc) > 1: - domain = proc[1] - else: - domain = '' - username = proc[0] - - if self.fixedPassword.value is not '': - password = self.fixedPassword.value - if self.fixedDomain.value is not '': - domain = self.fixedDomain.value - if self.useEmptyCreds.isTrue(): - username, password, domain = '', '', '' - - if self.withoutDomain.isTrue(): - domain = '' - - if '.' in domain: # Dotter domain form - username = username + '@' + domain - domain = '' - - # Fix username/password acording to os manager - username, password = service.processUserPassword(username, password) - - return {'protocol': self.protocol, 'username': username, 'password': password, 'domain': domain} + return self.processUserPassword(service, user, password) def renderForHtml(self, userService, transport, ip, os, user, password): # We use helper to keep this clean @@ -144,8 +94,3 @@ class RDPTransport(Transport): } return generateHtmlForRdp(self, userService.uuid, transport.uuid, os, ip, '3389', username, password, domain, extra) - - def getHtmlComponent(self, id, os, componentId): - logger.debug('Component: ID={}'.format(id)) - # We use helper to keep this clean - return getHtmlComponent(self.__module__, componentId) diff --git a/server/src/uds/transports/RDP/TSRDPTransport.py b/server/src/uds/transports/RDP/TSRDPTransport.py index a0b87e6f..162470a8 100644 --- a/server/src/uds/transports/RDP/TSRDPTransport.py +++ b/server/src/uds/transports/RDP/TSRDPTransport.py @@ -38,7 +38,8 @@ from uds.core.transports.BaseTransport import Transport from uds.core.transports import protocols from uds.core.util import connection from uds.core.util.Cache import Cache -from web import generateHtmlForRdp, getHtmlComponent +from .web import generateHtmlForRdp, getHtmlComponent +from .BaseRDPTransport import BaseRDPTransport import logging import random @@ -50,7 +51,7 @@ logger = logging.getLogger(__name__) READY_CACHE_TIMEOUT = 30 -class TSRDPTransport(Transport): +class TSRDPTransport(BaseRDPTransport): ''' Provides access via RDP to service. This transport can use an domain. If username processed by authenticator contains '@', it will split it and left-@-part will be username, and right password @@ -65,65 +66,28 @@ class TSRDPTransport(Transport): tunnelServer = gui.TextField(label=_('Tunnel server'), order=1, tooltip=_('IP or Hostname of tunnel server send to client device ("public" ip) and port. (use HOST:PORT format)')) tunnelCheckServer = gui.TextField(label=_('Tunnel host check'), order=2, tooltip=_('If not empty, this server will be used to check if service is running before assigning it to user. (use HOST:PORT format)')) - useEmptyCreds = gui.CheckBoxField(label=_('Empty creds'), order=3, tooltip=_('If checked, the credentials used to connect will be emtpy')) - fixedName = gui.TextField(label=_('Username'), order=4, tooltip=_('If not empty, this username will be always used as credential')) - fixedPassword = gui.PasswordField(label=_('Password'), order=5, tooltip=_('If not empty, this password will be always used as credential')) - withoutDomain = gui.CheckBoxField(label=_('Without Domain'), order=6, tooltip=_('If checked, the domain part will always be emptied (to connecto to xrdp for example is needed)')) - fixedDomain = gui.TextField(label=_('Domain'), order=7, tooltip=_('If not empty, this domain will be always used as credential (used as DOMAIN\\user)')) - allowSmartcards = gui.CheckBoxField(label=_('Allow Smartcards'), order=8, tooltip=_('If checked, this transport will allow the use of smartcards')) - allowPrinters = gui.CheckBoxField(label=_('Allow Printers'), order=9, tooltip=_('If checked, this transport will allow the use of user printers')) - allowDrives = gui.CheckBoxField(label=_('Allow Drives'), order=10, tooltip=_('If checked, this transport will allow the use of user drives')) - allowSerials = gui.CheckBoxField(label=_('Allow Serials'), order=11, tooltip=_('If checked, this transport will allow the use of user serial ports')) - wallpaper = gui.CheckBoxField(label=_('Show wallpaper'), order=12, tooltip=_('If checked, the wallpaper and themes will be shown on machine (better user experience, more bandwidth)')) + useEmptyCreds = BaseRDPTransport.useEmptyCreds + fixedName = BaseRDPTransport.fixedName + fixedPassword = BaseRDPTransport.fixedPassword + withoutDomain = BaseRDPTransport.withoutDomain + fixedDomain = BaseRDPTransport.fixedDomain + allowSmartcards = BaseRDPTransport.allowSmartcards + allowPrinters = BaseRDPTransport.allowPrinters + allowDrives = BaseRDPTransport.allowDrives + allowSerials = BaseRDPTransport.allowSerials + wallpaper = BaseRDPTransport.wallpaper def initialize(self, values): if values is not None: if values['tunnelServer'].count(':') != 1: raise Transport.ValidationException(_('Must use HOST:PORT in Tunnel Server Field')) - def isAvailableFor(self, ip): - ''' - Checks if the transport is available for the requested destination ip - Override this in yours transports - ''' - logger.debug('Checking availability for {0}'.format(ip)) - ready = self.cache().get(ip) - if ready is None: - # Check again for readyness - if connection.testServer(ip, '3389') == True: - self.cache().put(ip, 'Y', READY_CACHE_TIMEOUT) - return True - else: - self.cache().put(ip, 'N', READY_CACHE_TIMEOUT) - return ready == 'Y' - def renderForHtml(self, userService, transport, ip, os, user, password): # We use helper to keep this clean - username = user.getUsernameForAuth() prefs = user.prefs('rdp') - if self.fixedName.value is not '': - username = self.fixedName.value - - proc = username.split('@') - if len(proc) > 1: - domain = proc[1] - else: - domain = '' - username = proc[0] - if self.fixedPassword.value is not '': - password = self.fixedPassword.value - if self.fixedDomain.value is not '': - domain = self.fixedDomain.value - if self.useEmptyCreds.isTrue(): - username, password, domain = '', '', '' - - if '.' in domain: # Dotter domain form - username = username + '@' + domain - domain = '' - - if self.withoutDomain.isTrue(): - domain = '' + ci = self.getConnectionInfo(userService, user, password) + username, password, domain = ci['username'], ci['password'], ci['domain'] width, height = CommonPrefs.getWidthHeight(prefs) depth = CommonPrefs.getDepth(prefs) @@ -153,11 +117,4 @@ class TSRDPTransport(Transport): 'wallpaper': self.wallpaper.isTrue() } - # Fix username/password acording to os manager - username, password = userService.processUserPassword(username, password) - return generateHtmlForRdp(self, userService.uuid, transport.uuid, os, ip, '-1', username, password, domain, extra) - - def getHtmlComponent(self, id, os, componentId): - # We use helper to keep this clean - return getHtmlComponent(self.__module__, componentId) diff --git a/server/src/uds/web/views.py b/server/src/uds/web/views.py index 44887aae..544689e0 100644 --- a/server/src/uds/web/views.py +++ b/server/src/uds/web/views.py @@ -277,8 +277,9 @@ def service(request, idService, idTransport): itrans = trans.getInstance() if itrans.isAvailableFor(ip): log.doLog(ads, log.INFO, "User service ready, rendering transport", log.WEB) - transport = itrans.renderForHtml(ads, trans, ip, request.session['OS'], request.user, webPassword(request)) - return render_to_response(theme.template('show_transport.html'), {'transport': transport, 'nolang': True}, context_instance=RequestContext(request)) + transportHtml = itrans.renderForHtml(ads, trans, ip, request.session['OS'], request.user, webPassword(request)) + UserServiceManager.manager().notifyPreconnect(ads, itrans.processedUser(ads, request.user), itrans.protocol) + return render_to_response(theme.template('show_transport.html'), {'transport': transportHtml, 'nolang': True}, context_instance=RequestContext(request)) else: log.doLog(ads, log.WARN, "User service is not accessible (ip {0})".format(ip), log.TRANSPORT) logger.debug('Transport is not ready for user service {0}'.format(ads))