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:
commit
c2c5bc8aa1
@ -138,7 +138,7 @@ class Transports(ModelHandler):
|
||||
),
|
||||
'type': 'text',
|
||||
'order': 201,
|
||||
'tab': gettext(gui.Tab.ADVANCED),
|
||||
'tab': gui.Tab.ADVANCED,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]:
|
||||
|
@ -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,
|
||||
|
@ -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():
|
||||
|
@ -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");
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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',
|
||||
),
|
||||
|
@ -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 ''
|
||||
|
@ -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':
|
||||
|
@ -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')
|
||||
|
@ -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 = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user