1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-02-15 05:57:38 +03:00

Merged and fixed

This commit is contained in:
Adolfo Gómez García 2022-10-14 00:47:37 +02:00
commit c2c5bc8aa1
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
12 changed files with 164 additions and 56 deletions

View File

@ -138,7 +138,7 @@ class Transports(ModelHandler):
),
'type': 'text',
'order': 201,
'tab': gettext(gui.Tab.ADVANCED),
'tab': gui.Tab.ADVANCED,
},
)

View File

@ -115,7 +115,7 @@ class BaseModelHandler(Handler):
},
}
if field.get('tab', None):
v['gui']['tab'] = _(field['tab'])
v['gui']['tab'] = _(str(field['tab']))
gui.append(v)
return gui

View File

@ -129,15 +129,24 @@ def getRootUser() -> models.User:
# Decorator to make easier protect pages that needs to be logged in
def webLoginRequired(
admin: typing.Union[bool, str] = False
admin: typing.Union[bool, typing.Literal['admin']] = False
) -> typing.Callable[
[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]
]:
"""
Decorator to set protection to access page
"""Decorator to set protection to access page
Look for samples at uds.core.web.views
if admin == True, needs admin or staff
if admin == 'admin', needs admin
Args:
admin (bool, optional): If True, needs admin or staff. Is it's "admin" literal, needs admin . Defaults to False (any user).
Returns:
typing.Callable[[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]]: Decorator
Note:
This decorator is used to protect pages that needs to be logged in.
To protect against ajax calls, use `denyNonAuthenticated` instead
"""
def decorator(
@ -154,7 +163,7 @@ def webLoginRequired(
if not request.user or not request.authorized:
return HttpResponseRedirect(reverse('page.login'))
if admin is True or admin == 'admin': # bool or string "admin"
if admin in (True, 'admin'):
if request.user.isStaff() is False or (
admin == 'admin' and not request.user.is_admin
):
@ -178,7 +187,6 @@ def trustedSourceRequired(
) -> typing.Callable[..., HttpResponse]:
"""
Decorator to set protection to access page
look for sample at uds.dispatchers.pam
"""
@wraps(view_func)
@ -200,6 +208,8 @@ def trustedSourceRequired(
# decorator to deny non authenticated requests
# The difference with webLoginRequired is that this one does not redirect to login page
# it's designed to be used in ajax calls mainly
def denyNonAuthenticated(
view_func: typing.Callable[..., RT]
) -> typing.Callable[..., RT]:

View File

@ -120,7 +120,7 @@ class TicketStore(UUIDModel):
validator=validator,
validity=validity,
owner=owner,
).uuid
).uuid or ''
@staticmethod
def get(
@ -168,6 +168,47 @@ class TicketStore(UUIDModel):
except TicketStore.DoesNotExist:
raise TicketStore.InvalidTicket('Does not exists')
@staticmethod
def update(
uuid: str,
secure: bool = False,
owner: typing.Optional[str] = None,
checkFnc: typing.Callable[[typing.Any], bool] = lambda x: True,
**kwargs: typing.Any,
) -> None:
try:
t = TicketStore.objects.get(uuid=uuid)
data: bytes = t.data
if secure: # Owner has already been tested and it's not emtpy
if not owner:
raise ValueError('Tried to use a secure ticket without owner')
data = cryptoManager().AESDecrypt(
data, typing.cast(str, owner).encode()
)
dct = pickle.loads(data)
# invoke check function
if checkFnc(dct) is False:
raise TicketStore.InvalidTicket('Validation failed')
for k, v in kwargs.items():
if v is not None:
dct[k] = v
# Reserialize
data = pickle.dumps(dct)
if secure:
if not owner:
raise ValueError('Tried to use a secure ticket without owner')
data = cryptoManager().AESCrypt(data, owner.encode())
t.data = data
t.save(update_fields=['data'])
except TicketStore.DoesNotExist:
pass
@staticmethod
def revalidate(
uuid: str,
@ -193,6 +234,8 @@ class TicketStore(UUIDModel):
validity: int = 60 * 60 * 24, # 24 Hours default validity for tunnel tickets
) -> str:
owner = cryptoManager().randomString(length=8)
if not userService.user:
raise ValueError('User is not set in userService')
data = {
'u': userService.user.uuid if userService.user else '',
's': userService.uuid,

View File

@ -37,7 +37,7 @@ import csv
import datetime
import logging
from django.utils.translation import ugettext, ugettext_lazy as _
from django.utils.translation import gettext, gettext_lazy as _
from uds.core.ui import gui
from uds.core.util import log
@ -117,7 +117,7 @@ class ListReportAuditCSV(ListReport):
writer = csv.writer(output)
writer.writerow(
[ugettext('Date'), ugettext('Level'), ugettext('IP'), ugettext('User'), ugettext('Method'), ugettext('Response code'), ugettext('Request')]
[gettext('Date'), gettext('Level'), gettext('IP'), gettext('User'), gettext('Method'), gettext('Response code'), gettext('Request')]
)
for l in self.genData():

View File

@ -10,10 +10,14 @@ gettext("Service released");
gettext("Service reseted");
gettext("Are you sure?");
gettext("seconds");
gettext("Username");
gettext("Password");
gettext("Domain");
gettext("Your session has expired. Please, login again");
gettext("Error");
gettext("Please wait until the service is launched.");
gettext("Service ready");
gettext("Service ready");
gettext("UDS Client not launching");
gettext("UDS Client Download");
gettext("Error launching service");
@ -50,6 +54,7 @@ gettext("UDS Client");
gettext("About");
gettext("UDS Client");
gettext("About");
gettext("Please, enter access credentials");
gettext("You can access UDS Open Source code at");
gettext("UDS has been developed using these components:");
gettext("If you find that we missed any component, please let us know");

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# Copyright (c) 2012-2022 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import re
import logging
@ -41,9 +41,7 @@ from uds.models.util import getSqlDatetime
from django.utils.translation import gettext_noop as _
from uds.core.ui import gui
from uds.core import transports
from uds.core.util import os_detector as OsDetector
from uds.core.managers import cryptoManager
from uds import models
@ -327,12 +325,12 @@ class HTML5RDPTransport(transports.Transport):
raise transports.Transport.ValidationException(
_('The server must be http or https')
)
if self.useEmptyCreds.isTrue() and self.security.value != 'rdp':
raise transports.Transport.ValidationException(
_(
'Empty credentials (on Credentials tab) is only allowed with Security level (on Parameters tab) set to "RDP"'
)
)
#if self.useEmptyCreds.isTrue() and self.security.value != 'rdp':
# raise transports.Transport.ValidationException(
# _(
# 'Empty credentials (on Credentials tab) is only allowed with Security level (on Parameters tab) set to "RDP"'
# )
# )
# Same check as normal RDP transport
def isAvailableFor(self, userService: 'models.UserService', ip: str) -> bool:
@ -458,12 +456,12 @@ class HTML5RDPTransport(transports.Transport):
'create-drive-path': 'true',
'ticket-info': {
'userService': userService.uuid,
'user': userService.user.uuid if userService.user else '',
'user': user.uuid,
},
}
if password == '' and self.security.value != 'rdp':
extra_params='&' + urlencode({'username': username, 'domain': domain, 'reqcreds': 'true'})
extra_params=f'&creds={username}@{domain}'
else:
extra_params=''
@ -475,7 +473,7 @@ class HTML5RDPTransport(transports.Transport):
+ '_'
+ sanitize(user.name)
+ '/'
+ getSqlDatetime().strftime('%Y%m%d-%H%M')
+ models.getSqlDatetime().strftime('%Y%m%d-%H%M')
)
params['create-recording-path'] = 'true'

View File

@ -165,17 +165,17 @@ urlpatterns = [
),
# Enabler and Status action are first processed, and if not match, execute the generic "action" handler
re_path(
r'^uds/webapi/action/(?P<idService>.+)/enable/(?P<idTransport>[a-zA-Z0-9:-]+)$',
r'^uds/webapi/action/(?P<idService>[a-zA-Z0-9:-]+)/enable/(?P<idTransport>[a-zA-Z0-9:-]+)$',
uds.web.views.userServiceEnabler,
name='webapi.enabler',
),
re_path(
r'^uds/webapi/action/(?P<idService>.+)/status/(?P<idTransport>[a-zA-Z0-9:-]+)$',
r'^uds/webapi/action/(?P<idService>[a-zA-Z0-9:-]+)/status/(?P<idTransport>[a-zA-Z0-9:-]+)$',
uds.web.views.userServiceStatus,
name='webapi.status',
),
re_path(
r'^uds/webapi/action/(?P<idService>.+)/(?P<actionString>[a-zA-Z0-9:-]+)$',
r'^uds/webapi/action/(?P<idService>[a-zA-Z0-9:-]+)/(?P<actionString>[a-zA-Z0-9:-]+)$',
uds.web.views.action,
name='webapi.action',
),
@ -187,13 +187,19 @@ urlpatterns = [
),
# Transport own link processor
re_path(
r'^uds/webapi/trans/(?P<idService>.+)/(?P<idTransport>.+)$',
r'^uds/webapi/trans/(?P<idService>[a-zA-Z0-9:-]+)/(?P<idTransport>[a-zA-Z0-9:-]+)$',
uds.web.views.transportOwnLink,
name='TransportOwnLink',
),
# Transport ticket update (for username/password on html5)
re_path(
r'^uds/webapi/trans/ticket/(?P<idTicket>[a-zA-Z0-9:-]+)/(?P<scrambler>[a-zA-Z0-9:-]+)$',
uds.web.views.modern.update_transport_ticket,
name='webapi.transport.UpdateTransportTicket',
),
# Authenticators custom html
re_path(
r'^uds/webapi/customAuth/(?P<idAuth>.*)$',
r'^uds/webapi/customAuth/(?P<idAuth>[a-zA-Z0-9:-]*)$',
uds.web.views.customAuth,
name='uds.web.views.customAuth',
),

View File

@ -204,6 +204,7 @@ def udsJs(request: 'ExtendedHttpRequest') -> str:
),
'static': static(''),
'clientDownload': reverse('page.client-download'),
'updateTransportTicket': reverse('webapi.transport.UpdateTransportTicket', kwargs={'idTicket': 'param1', 'scrambler': 'param2'}),
# Launcher URL if exists
'launch': request.session.get('launch', ''),
'brand': settings.UDSBRAND if hasattr(settings, 'UDSBRAND') else ''

View File

@ -128,33 +128,31 @@ def exceptionView(request: 'HttpRequest', exception: Exception) -> HttpResponseR
logger.debug(traceback.format_exc())
try:
raise exception # Raise it so we can "catch" and redirect
except UserService.DoesNotExist:
return errorView(request, ERR_USER_SERVICE_NOT_FOUND)
except ServicePool.DoesNotExist: # type: ignore
return errorView(request, SERVICE_NOT_FOUND)
except Transport.DoesNotExist: # type: ignore
return errorView(request, TRANSPORT_NOT_FOUND)
except Authenticator.DoesNotExist: # type: ignore
return errorView(request, AUTHENTICATOR_NOT_FOUND)
except InvalidUserException:
if isinstance(exception, InvalidUserException):
return errorView(request, ACCESS_DENIED)
except InvalidServiceException:
return errorView(request, INVALID_SERVICE)
except MaxServicesReachedError:
return errorView(request, MAX_SERVICES_REACHED)
except InvalidAuthenticatorException:
elif isinstance(exception, InvalidAuthenticatorException):
return errorView(request, INVALID_CALLBACK)
except ServiceInMaintenanceMode:
elif isinstance(exception, InvalidServiceException):
return errorView(request, INVALID_SERVICE)
elif isinstance(exception, MaxServicesReachedError):
return errorView(request, MAX_SERVICES_REACHED)
elif isinstance(exception, ServiceInMaintenanceMode):
return errorView(request, SERVICE_IN_MAINTENANCE)
except ServiceNotReadyError as e:
# add code as high bits of idError
elif isinstance(exception, ServiceNotReadyError):
return errorView(request, SERVICE_NOT_READY)
except Exception as e:
logger.exception('Exception cautgh at view!!!')
return errorView(request, UNKNOWN_ERROR)
# raise e
elif isinstance(exception, UserService.DoesNotExist):
return errorView(request, ERR_USER_SERVICE_NOT_FOUND)
elif isinstance(exception, Transport.DoesNotExist):
return errorView(request, TRANSPORT_NOT_FOUND)
elif isinstance(exception, ServicePool.DoesNotExist):
return errorView(request, SERVICE_NOT_FOUND)
elif isinstance(exception, Authenticator.DoesNotExist):
return errorView(request, AUTHENTICATOR_NOT_FOUND)
logger.error(
'Unexpected exception: %s, traceback: %s', exception, traceback.format_exc()
)
return errorView(request, UNKNOWN_ERROR)
def error(request: 'HttpRequest', err: str) -> 'HttpResponse':

View File

@ -32,6 +32,7 @@ import time
import logging
import hashlib
import typing
import json
from django.middleware import csrf
from django.shortcuts import render
@ -45,6 +46,7 @@ from django.views.decorators.cache import never_cache
from uds.core.util.request import ExtendedHttpRequest, ExtendedHttpRequestWithUser
from uds.core.auths import auth, exceptions
from uds.core.managers import cryptoManager
from uds.web.util import errors
from uds.web.forms.LoginForm import LoginForm
from uds.web.forms.MFAForm import MFAForm
@ -54,14 +56,14 @@ from uds.web.util import configjs
from uds.core import mfas
logger = logging.getLogger(__name__)
CSRF_FIELD = 'csrfmiddlewaretoken'
MFA_COOKIE_NAME = 'mfa_status'
if typing.TYPE_CHECKING:
from uds import models
pass
@never_cache
def index(request: HttpRequest) -> HttpResponse:
@ -172,12 +174,12 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
): # If no user, or user is already authorized, redirect to index
return HttpResponseRedirect(reverse('page.index')) # No user, no MFA
mfaProvider: 'models.MFA' = request.user.manager.mfa
mfaProvider: typing.Optional['models.MFA'] = request.user.manager.mfa
if not mfaProvider:
return HttpResponseRedirect(reverse('page.index'))
userHashValue: str = hashlib.sha3_256(
(request.user.name + request.user.uuid + mfaProvider.uuid).encode()
(request.user.name + (request.user.uuid or '') + mfaProvider.uuid).encode()
).hexdigest()
# Try to get cookie anc check it
@ -295,3 +297,46 @@ def mfa(request: ExtendedHttpRequest) -> HttpResponse:
'html': mfaHtml,
}
return index(request) # Render index with MFA data
@csrf_exempt
@auth.denyNonAuthenticated
def update_transport_ticket(request: ExtendedHttpRequestWithUser, idTicket: str, scrambler: str) -> HttpResponse:
try:
if request.method == 'POST':
# Get request body as json
data = json.loads(request.body)
# Update username andd password in ticket
username = data.get('username', None) or None # None if not present
password = data.get('password', None) or None # If password is empty, set it to None
domain = data.get('domain', None) or None # If empty string, set to None
if password:
password = cryptoManager().symCrypt(password, scrambler)
def checkValidTicket(data: typing.Mapping[str, typing.Any]) -> bool:
if 'ticket-info' not in data:
return True
try:
user = models.User.objects.get(uuid=data['ticket-info'].get('user', None))
if request.user == user:
return True
except models.User.DoesNotExist:
pass
return False
models.TicketStore.update(
uuid=idTicket,
checkFnc=checkValidTicket,
username=username,
password=password,
domain=domain,
)
return HttpResponse('{"status": "OK"}', status=200, content_type='application/json')
except Exception as e:
# fallback to error
pass
# Invalid request
return HttpResponse('{"status": "Invalid Request"}', status=400, content_type='application/json')

View File

@ -62,12 +62,14 @@ def transportOwnLink(
):
response: typing.MutableMapping[str, typing.Any] = {}
# If userService is not owned by user, will raise an exception
# For type checkers to "be happy"
try:
res = userServiceManager().getService(
request.user, request.os, request.ip, idService, idTransport
)
ip, userService, iads, trans, itrans = res # pylint: disable=unused-variable
ip, userService, iads, trans, itrans = res
# This returns a response object in fact
if itrans and ip:
response = {