1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-08 21:18:00 +03:00

Merge branch 'master' into linux-client-DC

This commit is contained in:
Alexander Burmatov 2023-05-17 17:14:48 +03:00 committed by GitHub
commit afb1d2cafe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
433 changed files with 10176 additions and 7864 deletions

View File

@ -1,6 +1,8 @@
![UDS Logo](https://www.udsenterprise.com/static//img/logoUDSNav.png)
openuds
The main repository has been transfered to https://github.com/VirtualCable/openuds
OpenUDS
=======
OpenUDS (Universal Desktop Services) is a multiplatform connection broker for:
@ -13,3 +15,4 @@ This is an Open Source Source project, initiated by Spanish Company Virtualca
Please fell free to contribute to this project.
**Note: Master version is always under heavy development and it is not recommended for use, it will probably have unfixed bugs. Please use the latest stable branch.**

11
SECURITY.md Normal file
View File

@ -0,0 +1,11 @@
# Security
Virtual Cable takes the security of our software products and services seriously.
If you find any vulnerability, please, report it to "agomez@virtualcable.net".
Please, do not use the issue tracker for security vulnerabilities.
[]: # Path: README.md
Thank you very much for your interest in OpenUDS.

View File

@ -1 +0,0 @@

View File

@ -27,7 +27,7 @@
# 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
Author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import sys
import os

View File

@ -239,7 +239,7 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
pixmap: 'QPixmap' = self._qApp.primaryScreen().grabWindow(0) # type: ignore
ba = QByteArray()
buffer = QBuffer(ba)
buffer.open(QIODevice.WriteOnly) # type: ignore
buffer.open(QIODevice.OpenModeFlag.WriteOnly)
pixmap.save(buffer, 'PNG')
buffer.close()
scrBase64 = bytes(ba.toBase64()).decode() # type: ignore # there are problems with Pylance and connects on PyQt5... :)

View File

@ -1,5 +1,7 @@
from .. import types
# Default certificate, will be overwritten by the first call to Broker, it's needed to wake up the server part of the actor
# at the beginning, but will be replaced by the real certificate.
defaultCertificate = types.CertificateInfoType(
private_key='-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFHTBPBgkqhkiG9w0BBQ0wQjApBgkqhkiG9w0BBQwwHAQIfG2+iMYJBswCAggA\nMAwGCCqGSIb3DQIJBQAwFQYJKwYBBAGXVQECBAhCusU5R8ulZQSCBMgheyZ81Qkq\n+TcbPeBlUGCFllSUOo7xQ/OuwYSmzLx8LpN0hQNv4azF6MYH+I8eMSPd3A547yW3\nJE4GjIBfRvcq2X1UZ2FQfECU9UP0ShPuPrVhIh6ZZklmlRjbIF8hGfSzXAuafQb+\n4wXXsofahi/SPgqK1Gw65nRiMcoeRZchJkx8pBgKVWED6Cbh6aAkeqkVKPnsebiV\n6kE+0C7+hgNUbyRd46R+/5NXzPjg4ItfSak+PLzQ1KeRv4Cu6DdzRKJ4V9/MlNdU\nNNEkSVSEaRn4sv+eByU4uxBMaSmD1tLc/A7OmaAeRpIQvls3Zcf2+V0+anAtjbjd\n6eIb2nceey+dKFm4ewlR4mXuzj1QowRTHceOIkvKIrOODxdy9M5hNBZ7VLum29tY\nRhqtmEH2BZZJ8SpM2SsEZzPxqJFiVZbvpeOKjxlMyn1dFWn1rP8uMnfuMKqBaj5D\nd5clOPlwebYw5UpM6Vvawu4nGqxECTSWcfNlDYO5U/0Fsm9+JIrJ7Buukgv2+rhs\nD/6oUK9NB8AW9qnDr7UxbC/ujhkKQG3woaZlPbiMs5WQaS+DrTg4N49wPzS0h+ME\nF8ZzuPnd6+sMGQioCIrQAZ08rk54oCijBhFh8/EQhQKGsMFw2swi9t6+FVU5Bvil\nlhmBd3LA5EuQ5y1X0jRL/+GDiUiZw1gOJP8d/XzhUJL9AmamdqJ6/rAU7lUTNWkM\ndzmFonUO2Mh2zgEEudHsTOH8udZ2l64LIHc6fCkDmM8QzghjrEFyci6R8333DSSM\nwbM0MvyTLM7TTqZUD60EgD+Ihyr/wJcBZY7GVn7hTq7ee14zeI+dZFmTMYOnt0mA\ngof19t0naPPZU+zyl/ambNF5mmSkGOAl4IBHNvPt5ztEVbNpwW3DHbmdYW71Ax+z\nCDlr4iKZahv21o1PCesPV2IlaHZFD6aBRt0DxzMqtq9cpWsI1g7aEaAjRbSvqhMY\npUeqFXz/GfR9rjRkufr48//ll0/Q/Ogx7m1TjQ6mAEQrklI7pa2W0u3H0BpSZSis\nR6ST3ulE+wfsp8cau6q2er+BSsDhBjSn9FeCUjHzY56u9ud/kb6/jLEdgxNpj0na\n3WVqCCCL/dAFSWznBmdracZsRMXapXInHCiiOEkXXbXIXvRKiTPJXdN+w2/U2j2B\nwXZuazVSpmM+xAZTAS9dtBUQJo+5px9b6P09uagvTA32ezbpPXf+hSfmTdUwbmAY\nrmE9SW85tzX+cD17loygBBRrjOr4uQy/s/9FqLx8bM73jly05rdOmX28ECKwEA05\n8aCFkfqrl9J9doVapaUlywpJVPFtE6W6tCF+ULMfb16vEjT1du1+epEnbGGLRQxg\n3aFLyKlvFaNvR38fiQFUGtBgGOaBN3rhGpbMwjch3oReXv9X/4UCL6sVIiOH2H3c\nVSZdC3O5g6CMVe4zckUe1k9mLDb5524IHDFfptZ6Bw+uzrqIy3GHW8dJF2AK471b\nMUnCojTpdbFHaUs2u/rNKVUyY+vLf8hkyP+znBUoPxSJtty53EWNukxjjsxx0lx3\niZGqN72lXlXuSFZAIxi307+xxE21cbzDsMidyJkbKKGm/F4BOKvX9jWmAyYmBG6A\n1L3yNRouFWsYDwYAX2nZ1is=\n-----END ENCRYPTED PRIVATE KEY-----\n',
server_certificate='-----BEGIN CERTIFICATE-----\nMIIDcTCCAlkCBDfnXU8wDQYJKoZIhvcNAQELBQAwfTELMAkGA1UEBhMCRVMxDzAN\nBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFkcmlkMREwDwYDVQQKDAhVRFMgQ2Vy\ndDERMA8GA1UECwwIVURTIENlcnQxEjAQBgNVBAMMCTEyNy4wLjAuMTESMBAGA1Ud\nEQwJMTI3LjAuMC4xMB4XDTIwMDIxNzExNTkzMloXDTMwMDIxNDExNTkzMlowfTEL\nMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFkcmlkMREw\nDwYDVQQKDAhVRFMgQ2VydDERMA8GA1UECwwIVURTIENlcnQxEjAQBgNVBAMMCTEy\nNy4wLjAuMTESMBAGA1UdEQwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA2e1cW7YtRpNLazR3f/LqLv8OB0rKh8cUPH4wuQhbBTkee8Wu\n5eMSadRCIyRbKj4b8dtVfI9QW0SrmhGuMx1KCh3CsYd9XsWiKbGkiRBHIDOn5pkF\n6PUayDJ8KjnGbfnZjp0AmxXP4r1OO8jUPqzKS9Ubf5PgwcwdFiUKVfVPwGwctwt5\nt9YpSRONw0rTsCjVHvO2dd9h6EopskLCWxpN8l9kNLwLM/6t0IqVKmn5/IYPKKN2\nCX8a7IXpxwoiUs4sBZYhUMBWikB1hKQRSYafp1Xvc5PeTFXTFqGANnqz0NoZ8tqL\n8qjQUN/PCdtzhfcP5RgT2g1qyS2RBCMYH7Zs0wIDAQABMA0GCSqGSIb3DQEBCwUA\nA4IBAQCUt+qlLA1N9VXMwDQAYG4Kt6/UlMHCXAajHQQGtjdyGJ4++m7EIjI96hMU\n3Cx2gp2ggR3JGnuSR+DdBvPl5iGku7J8KV0JiJg30gTY8JuUIy/PMLZWloYKrBHV\nlin2GujQ4OsIt3dbr4XtcKW1Wd7L6fBzHlq7Xyxh+gcTzTvTmq67Q9XKlBWsegMf\nv4FKy0lfcSFK3vTzswQtuTontG4TqLiT/4AnMt3D0cTQ6b6KoZwUUX/TDNhau06d\nQ4Ilz8X61ka+4HBkFSR5ahP9noCVhwO329h+6epO141E5Tep3OLc/GCF4oaKOlMR\nfqxf5f2bghU0fxmtEoNJTZkBsN1S\n-----END CERTIFICATE-----\n',

View File

@ -37,9 +37,9 @@ from udsactor import tools, types
from udsactor.log import logger
# For avoid proxy on localhost connections
NO_PROXY = {
'http': None,
'https': None,
NO_PROXY: typing.Dict[str, str] = {
'http': '',
'https': '',
}

View File

@ -42,12 +42,10 @@ from .. import rest
from .public import PublicProvider
from .local import LocalProvider
# a couple of 1.2 ciphers + 1.3 ciphers (implicit)
DEFAULT_CIPHERS = (
'ECDHE-RSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES256-CCM'
':DHE-RSA-AES256-SHA256'
'ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
)
# Not imported at runtime, just for type checking
@ -191,8 +189,8 @@ class HTTPServerThread(threading.Thread):
# self._server.socket = ssl.wrap_socket(self._server.socket, certfile=self.certFile, server_side=True)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# Disable TLSv1.0 and TLSv1.1, disable TLSv1.2, use only TLSv1.3
context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
# Disable TLSv1.0 and TLSv1.1, use only TLSv1.3 or TLSv1.2 with allowed ciphers
context.minimum_version = ssl.TLSVersion.TLSv1_2
# If a configures ciphers are provided, use them, otherwise use the default ones
context.set_ciphers(self._service._certificate.ciphers or DEFAULT_CIPHERS)

View File

@ -56,9 +56,7 @@ def _getMacAddr(ifname: str) -> typing.Optional[str]:
ifnameBytes = ifname.encode('utf-8')
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = bytearray(
fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifnameBytes[:15]))
)
info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifnameBytes[:15])))
return str(''.join(['%02x:' % char for char in info[18:24]])[:-1]).upper()
except Exception:
return None
@ -110,10 +108,7 @@ def _getInterfaces() -> typing.List[str]:
)[0]
namestr = names.tobytes()
# return namestr, outbytes
return [
namestr[i : i + offset].split(b'\0', 1)[0].decode('utf-8')
for i in range(0, outbytes, length)
]
return [namestr[i : i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
def _getIpAndMac(
@ -138,10 +133,7 @@ def getNetworkInfo() -> typing.Iterator[types.InterfaceInfoType]:
for ifname in _getInterfaces():
ip, mac = _getIpAndMac(ifname)
if (
mac != '00:00:00:00:00:00'
and mac
and ip
and ip.startswith('169.254') is False
mac != '00:00:00:00:00:00' and mac and ip and ip.startswith('169.254') is False
): # Skips local interfaces & interfaces with no dhcp IPs
yield types.InterfaceInfoType(name=ifname, mac=mac, ip=ip)
@ -164,6 +156,7 @@ def getLinuxOs() -> str:
def getVersion() -> str:
return 'Linux ' + getLinuxOs()
def reboot(flags: int = 0):
'''
Simple reboot using os command
@ -173,6 +166,7 @@ def reboot(flags: int = 0):
except Exception as e:
logger.error('Error rebooting: %s', e)
def loggoff() -> None:
'''
Right now restarts the machine...
@ -193,7 +187,6 @@ def renameComputer(newName: str) -> bool:
rename(newName)
return True # Always reboot right now. Not much slower but much more convenient
def joinDomain( # pylint: disable=unused-argument, too-many-arguments
name: str,
domain: str,
@ -256,15 +249,20 @@ def leaveDomain(
except Exception as e:
logger.error(f'Error leave machine from domain {domain}: {e}')
def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None:
def changeUserPassword(
user: str, oldPassword: str, newPassword: str
) -> None: # pylint: disable=unused-argument
'''
Simple password change for user using command line
Simple password change for user on linux
'''
subprocess.run( # nosec: Fine, all under control
'echo "{1}\n{1}" | /usr/bin/passwd {0} 2> /dev/null'.format(user, newPassword),
shell=True,
)
try:
subprocess.Popen(
['/usr/bin/passwd', user], # nosec: Fixed params
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
).communicate(f'{newPassword}\n{newPassword}\n'.encode('utf-8'))
except Exception as e:
logger.error('Error changing password: %s', e)
def initIdleDuration(atLeastSeconds: int) -> None:
@ -289,11 +287,7 @@ def getSessionType() -> str:
* xrdp --> xrdp session
* other types
'''
return (
'xrdp'
if 'XRDP_SESSION' in os.environ
else os.environ.get('XDG_SESSION_TYPE', 'unknown')
)
return 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown')
def forceTimeSync() -> None:

View File

@ -32,10 +32,12 @@
# pylint: disable=invalid-name
import warnings
import json
import ssl
import logging
import typing
import requests
import requests.adapters
from udsactor import types, tools
from udsactor.version import VERSION, BUILD
@ -94,6 +96,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
_host: str = ''
_validateCert: bool = True
_url: str = ''
_session: 'requests.Session'
def __init__(self, host: str, validateCert: bool) -> None:
self._host = host
@ -107,6 +110,28 @@ class UDSApi: # pylint: disable=too-few-public-methods
except Exception: # nosec: not interested in exceptions
pass
context = (
ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
if validateCert
else ssl._create_unverified_context(purpose=ssl.Purpose.SERVER_AUTH, check_hostname=False)
)
# Disable SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2
context.minimum_version = ssl.TLSVersion.TLSv1_3
# Configure session security
class UDSHTTPAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs) -> None:
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)
def cert_verify(self, conn, url, verify, cert): # pylint: disable=unused-argument
# Overridden to do nothing
return super().cert_verify(conn, url, validateCert, cert)
self._session = requests.Session()
self._session.mount("https://", UDSHTTPAdapter())
@property
def _headers(self) -> typing.MutableMapping[str, str]:
return {
@ -126,11 +151,11 @@ class UDSApi: # pylint: disable=too-few-public-methods
) -> typing.Any:
headers = headers or self._headers
try:
result = requests.post(
result = self._session.post(
self._api_url(method),
data=json.dumps(payLoad),
headers=headers,
verify=self._validateCert,
# verify=self._validateCert, Not needed, already in session
timeout=TIMEOUT,
proxies=NO_PROXY # type: ignore
if disableProxy
@ -163,10 +188,10 @@ class UDSServerApi(UDSApi):
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try:
result = requests.get(
result = self._session.get(
self._url + 'auth/auths',
headers=self._headers,
verify=self._validateCert,
# verify=self._validateCert,
timeout=4,
)
if result.ok:
@ -179,7 +204,7 @@ class UDSServerApi(UDSApi):
priority=v['priority'],
isCustom=v['isCustom'],
)
except Exception: # nosec: not interested in exceptions
except Exception as e:
pass
def register(
@ -214,22 +239,22 @@ class UDSServerApi(UDSApi):
# First, try to login
authInfo = {'auth': auth, 'username': username, 'password': password}
headers = self._headers
result = requests.post(
result = self._session.post(
self._url + 'auth/login',
data=json.dumps(authInfo),
headers=headers,
verify=self._validateCert,
# verify=self._validateCert,
)
if not result.ok or result.json()['result'] == 'error':
raise Exception() # Invalid credentials
headers['X-Auth-Token'] = result.json()['token']
result = requests.post(
result = self._session.post(
self._api_url('register'),
data=json.dumps(data),
headers=headers,
verify=self._validateCert,
# verify=self._validateCert,
)
if result.ok:
return result.json()['result']

View File

@ -90,7 +90,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._clientsPool = clients_pool.UDSActorClientPool()
self._certificate = (
cert.defaultCertificate
) # For being used on "unmanaged" hosts only
) # For being used on "unmanaged" hosts only, and prior to first login
self._http = None
# Initialzies loglevel and serviceLogger

View File

@ -1,10 +1,10 @@
Source: udsclient3
Section: admin
Priority: optional
Maintainer: Adolfo Gómez García <agomez@virtualcable.es>
Maintainer: Adolfo Gómez García <agomez@virtualcable.net>
Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2
Homepage: http://www.virtualcable.es
Homepage: http://www.udsenterprise.com
Package: udsclient3
Section: admin

View File

@ -5,9 +5,9 @@ Source: http://github.com/dkmstr/openuds/client-py3
Files: *
Copyright: (c) 2014-2022, Virtual Cable S.L.U.
License: 3-BSD
License: BSD-3-clause
License: 3-BSD
License: BSD-3-clause
All rights reserved.
.
Redistribution and use in source and binary forms, with or without
@ -35,4 +35,4 @@ License: 3-BSD
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.

View File

@ -1,2 +1,4 @@
#!/bin/sh
cp /UDSClient/UDSClient.desktop /usr/share/applications.mime
chmod 755 /UDSClient/UDSClient

View File

@ -9,6 +9,7 @@ fi
echo "Installing UDSClient Portable..."
cp UDSClient-0.0.0-x86_64.AppImage /usr/bin
chmod 755 /usr/bin/UDSClient-0.0.0-x86_64.AppImage
cp UDSClient.desktop /usr/share/applications
update-desktop-database

View File

@ -1,3 +1,5 @@
UDSClient is the client connector needed to get acccess to services managed by UDS Broker.
For raspberry Pi, AppImage does not works with 1.1.0 (works with 1.0.3)
Please, visit http://www.udsenterprise.com for more information

View File

@ -41,11 +41,11 @@ import typing
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import QSettings
from uds.rest import RestApi, RetryException, InvalidVersion, UDSException
from uds.rest import RestApi, RetryException, InvalidVersion
# Just to ensure there are available on runtime
from uds.forward import forward as ssh_forward # type: ignore
from uds.tunnel import forward as tunnel_forwards # type: ignore
from uds.forward import forward as ssh_forward # type: ignore # pylint: disable=unused-import
from uds.tunnel import forward as tunnel_forwards # type: ignore # pylint: disable=unused-import
from uds.log import logger
from uds import tools
@ -55,7 +55,6 @@ from UDSWindow import Ui_MainWindow
class UDSClient(QtWidgets.QMainWindow):
ticket: str = ''
scrambler: str = ''
withError = False
@ -149,7 +148,7 @@ class UDSClient(QtWidgets.QMainWindow):
webbrowser.open(e.downloadUrl)
self.closeWindow()
return
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
self.showError(e)
self.closeWindow()
return
@ -168,7 +167,9 @@ class UDSClient(QtWidgets.QMainWindow):
# self.hide()
self.closeWindow()
exec(script, globals(), {'parent': self, 'sp': params})
exec(
script, globals(), {'parent': self, 'sp': params}
) # pylint: disable=exec-used
# Execute the waiting tasks...
threading.Thread(target=endScript).start()
@ -177,7 +178,8 @@ class UDSClient(QtWidgets.QMainWindow):
self.ui.info.setText(str(e) + ', retrying access...')
# Retry operation in ten seconds
QtCore.QTimer.singleShot(10000, self.getTransportData)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception('Error getting transport data')
self.showError(e)
def start(self):
@ -194,27 +196,27 @@ def endScript():
try:
# Remove early stage files...
tools.unlinkFiles(early=True)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('Unlinking files on early stage: %s', e)
# After running script, wait for stuff
try:
logger.debug('Wating for tasks to finish...')
tools.waitForTasks()
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('Watiting for tasks to finish: %s', e)
try:
logger.debug('Unlinking files')
tools.unlinkFiles(early=False)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('Unlinking files on later stage: %s', e)
# Removing
try:
logger.debug('Executing threads before exit')
tools.execBeforeExit()
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.debug('execBeforeExit: %s', e)
logger.debug('endScript done')
@ -305,7 +307,7 @@ def minimal(api: RestApi, ticket: str, scrambler: str):
+ '\n\nPlease, retry again in a while.',
QtWidgets.QMessageBox.Ok,
)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
# logger.exception('Got exception on getTransportData')
QtWidgets.QMessageBox.critical(
None, # type: ignore
@ -352,31 +354,38 @@ def main(args: typing.List[str]):
sys.exit(0)
logger.debug('URI: %s', uri)
if uri[:6] != 'uds://' and uri[:7] != 'udss://':
raise Exception()
# Shows error if using http (uds:// ) version, not supported anymore
if uri[:6] == 'uds://':
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Notice',
f'UDS Client Version {VERSION} does not support HTTP protocol Anymore.',
QtWidgets.QMessageBox.Ok,
)
sys.exit(1)
if uri[:7] != 'udss://':
raise Exception('Not supported protocol') # Just shows "about" dialog
ssl = uri[3] == 's'
host, ticket, scrambler = uri.split('//')[1].split('/') # type: ignore
logger.debug(
'ssl:%s, host:%s, ticket:%s, scrambler:%s',
ssl,
'host:%s, ticket:%s, scrambler:%s',
host,
ticket,
scrambler,
)
except Exception:
except Exception: # pylint: disable=broad-except
logger.debug('Detected execution without valid URI, exiting')
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Notice',
'UDS Client Version {}'.format(VERSION),
f'UDS Client Version {VERSION}',
QtWidgets.QMessageBox.Ok,
)
sys.exit(1)
# Setup REST api endpoint
api = RestApi(
'{}://{}/uds/rest/client'.format(['http', 'https'][ssl], host), sslError
f'https://{host}/uds/rest/client', sslError
)
try:
@ -394,7 +403,7 @@ def main(args: typing.List[str]):
exitVal = app.exec()
logger.debug('Execution finished correctly')
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
logger.exception('Got an exception executing client:')
exitVal = 128
QtWidgets.QMessageBox.critical(
@ -404,5 +413,6 @@ def main(args: typing.List[str]):
logger.debug('Exiting')
sys.exit(exitVal)
if __name__ == "__main__":
main(sys.argv)

View File

@ -216,7 +216,7 @@ class ForwardThread(threading.Thread):
class SubHandler(Handler):
chain_host = self.redirectHost
chain_port = self.redirectPort
ssh_transport = self.client.get_transport()
ssh_transport = self.client.get_transport() # type: ignore
event = self.stopEvent
thread = self

View File

@ -51,6 +51,19 @@ from .log import logger
# Server before this version uses "unsigned" scripts
OLD_METHOD_VERSION = '2.4.0'
SECURE_CIPHERS = (
'TLS_AES_256_GCM_SHA384'
':TLS_CHACHA20_POLY1305_SHA256'
':TLS_AES_128_GCM_SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
':ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-CHACHA20-POLY1305'
':ECDHE-ECDSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES128-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305'
)
# Callback for error on cert
# parameters are hostname, serial
# If returns True, ignores error
@ -72,7 +85,6 @@ class InvalidVersion(UDSException):
super().__init__(downloadUrl)
self.downloadUrl = downloadUrl
class RestApi:
_restApiUrl: str # base Rest API URL
@ -184,6 +196,10 @@ class RestApi:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# Disable SSLv2, SSLv3, TLSv1, TLSv1.1
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.set_ciphers(SECURE_CIPHERS)
# If we have the certificates file, we use it
if tools.getCaCertsFile() is not None:
ctx.load_verify_locations(tools.getCaCertsFile())

View File

@ -44,11 +44,17 @@ import typing
import certifi
# For signature checking
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import utils, padding
try:
import psutil
except ImportError:
psutil = None
from .log import logger
_unlinkFiles: typing.List[typing.Tuple[str, bool]] = []
@ -76,9 +82,7 @@ nVgtClKcDDlSaBsO875WDR0CAwEAAQ==
def saveTempFile(content: str, filename: typing.Optional[str] = None) -> str:
if filename is None:
filename = ''.join(
random.choice(string.ascii_lowercase + string.digits) for _ in range(16)
)
filename = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
filename = filename + '.uds'
filename = os.path.join(tempfile.gettempdir(), filename)
@ -108,9 +112,7 @@ def testServer(host: str, port: typing.Union[str, int], timeOut: int = 4) -> boo
return True
def findApp(
appName: str, extraPath: typing.Optional[str] = None
) -> typing.Optional[str]:
def findApp(appName: str, extraPath: typing.Optional[str] = None) -> typing.Optional[str]:
searchPath = os.environ['PATH'].split(os.pathsep)
if extraPath:
searchPath += list(extraPath)
@ -139,9 +141,7 @@ def addFileToUnlink(filename: str, early: bool = False) -> None:
'''
Adds a file to the wait-and-unlink list
'''
logger.debug(
'Added file %s to unlink on %s stage', filename, 'early' if early else 'later'
)
logger.debug('Added file %s to unlink on %s stage', filename, 'early' if early else 'later')
_unlinkFiles.append((filename, early))
@ -195,9 +195,7 @@ def waitForTasks() -> None:
psutil.process_iter(attrs=('ppid',)),
)
)
logger.debug(
'Waiting for subprocesses... %s, %s', task.pid, subProcesses
)
logger.debug('Waiting for subprocesses... %s, %s', task.pid, subProcesses)
for i in subProcesses:
logger.debug('Found %s', i)
i.wait()
@ -224,14 +222,7 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
param: signature String signature to be verified
return: Boolean. True if the signature is valid; False otherwise.
'''
# For signature checking
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import utils, padding
public_key = serialization.load_pem_public_key(
data=PUBLIC_KEY, backend=default_backend()
)
public_key = serialization.load_pem_public_key(data=PUBLIC_KEY, backend=default_backend())
try:
public_key.verify( # type: ignore
@ -261,9 +252,17 @@ def getCaCertsFile() -> typing.Optional[str]:
# Check if "standard" paths are valid for linux systems
if 'linux' in sys.platform:
for path in ('/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/ca-bundle.pem'):
for path in (
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/certs/ca-certificates.crt',
'/etc/ssl/ca-bundle.pem',
):
if os.path.exists(path):
logger.info('Found certifi path: %s', path)
return path
return None
def isMac() -> bool:
return 'darwin' in sys.platform

View File

@ -32,8 +32,6 @@ import socket
import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
@ -48,13 +46,15 @@ BUFFER_SIZE: typing.Final[int] = 1024 * 16 # Max buffer length
LISTEN_ADDRESS: typing.Final[str] = '0.0.0.0' if DEBUG else '127.0.0.1'
LISTEN_ADDRESS_V6: typing.Final[str] = '::' if DEBUG else '::1'
# ForwarServer states
class ForwardState(enum.IntEnum):
class ForwardState(enum.IntEnum):
TUNNEL_LISTENING = 0
TUNNEL_OPENING = 1
TUNNEL_PROCESSING = 2
TUNNEL_ERROR = 3
# Some constants strings for protocol
HANDSHAKE_V1: typing.Final[bytes] = b'\x5AMGB\xA5\x01\x00'
CMD_TEST: typing.Final[bytes] = b'TEST'
@ -62,8 +62,10 @@ CMD_OPEN: typing.Final[bytes] = b'OPEN'
RESPONSE_OK: typing.Final[bytes] = b'OK'
logger = logging.getLogger(__name__)
PayLoadType = typing.Optional[typing.Tuple[typing.Optional[bytes], typing.Optional[bytes]]]
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
@ -74,9 +76,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
ticket: str
stop_flag: threading.Event
can_stop: bool
timeout: int
timer: typing.Optional[threading.Timer]
check_certificate: bool
keep_listening: bool
current_connections: int
status: ForwardState
@ -87,39 +89,42 @@ class ForwardServer(socketserver.ThreadingTCPServer):
remote: typing.Tuple[str, int],
ticket: str,
timeout: int = 0,
local_port: int = 0,
local_port: int = 0, # Use first available listen port if not specified
check_certificate: bool = True,
keep_listening: bool = False,
ipv6_listen: bool = False,
ipv6_remote: bool = False,
) -> None:
local_port = local_port or random.randrange(33000, 53000)
) -> None:
# Negative values for timeout, means "accept always connections"
# "but if no connection is stablished on timeout (positive)"
# "stop the listener"
# Note that this is for backwards compatibility, better use "keep_listening"
if timeout < 0:
keep_listening = True
timeout = abs(timeout)
if ipv6_listen:
self.address_family = socket.AF_INET6
# Binds and activate the server, so if local_port is 0, it will be assigned
super().__init__(
server_address=(LISTEN_ADDRESS if ipv6_listen else LISTEN_ADDRESS_V6, local_port),
server_address=(LISTEN_ADDRESS_V6 if ipv6_listen else LISTEN_ADDRESS, local_port),
RequestHandlerClass=Handler,
)
self.remote = remote
self.remote_ipv6 = ipv6_remote or ':' in remote[0] # if ':' in remote address, it's ipv6 (port is [1])
self.ticket = ticket
# Negative values for timeout, means "accept always connections"
# "but if no connection is stablished on timeout (positive)"
# "stop the listener"
self.timeout = int(time.time()) + timeout if timeout > 0 else 0
self.check_certificate = check_certificate
self.keep_listening = keep_listening
self.stop_flag = threading.Event() # False initial
self.current_connections = 0
self.status = ForwardState.TUNNEL_LISTENING
self.can_stop = False
timeout = abs(timeout) or 60
self.timer = threading.Timer(
abs(timeout), ForwardServer.__checkStarted, args=(self,)
)
timeout = timeout or 60
self.timer = threading.Timer(timeout, ForwardServer.__checkStarted, args=(self,))
self.timer.start()
def stop(self) -> None:
@ -132,7 +137,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
self.shutdown()
def connect(self) -> ssl.SSLSocket:
with socket.socket(socket.AF_INET6 if self.remote_ipv6 else socket.AF_INET, socket.SOCK_STREAM) as rsocket:
with socket.socket(
socket.AF_INET6 if self.remote_ipv6 else socket.AF_INET, socket.SOCK_STREAM
) as rsocket:
logger.info('CONNECT to %s', self.remote)
rsocket.connect(self.remote)
@ -143,10 +150,13 @@ class ForwardServer(socketserver.ThreadingTCPServer):
# Do not "recompress" data, use only "base protocol" compression
context.options |= ssl.OP_NO_COMPRESSION
# Macs with default installed python, does not support mininum tls version set to TLSv1.3
# USe "brew" version instead, or uncomment next line and comment the next one
# context.minimum_version = ssl.TLSVersion.TLSv1_2 if tools.isMac() else ssl.TLSVersion.TLSv1_3
context.minimum_version = ssl.TLSVersion.TLSv1_3
if tools.getCaCertsFile() is not None:
context.load_verify_locations(
tools.getCaCertsFile()
) # Load certifi certificates
context.load_verify_locations(tools.getCaCertsFile()) # Load certifi certificates
# If ignore remote certificate
if self.check_certificate is False:
@ -171,18 +181,20 @@ class ForwardServer(socketserver.ThreadingTCPServer):
logger.debug('Tunnel is available!')
return True
except Exception as e:
logger.error(
'Error connecting to tunnel server %s: %s', self.server_address, e
)
logger.error('Error connecting to tunnel server %s: %s', self.server_address, e)
return False
@property
def stoppable(self) -> bool:
logger.debug('Is stoppable: %s', self.can_stop)
return self.can_stop or (self.timeout != 0 and int(time.time()) > self.timeout)
return self.can_stop
@staticmethod
def __checkStarted(fs: 'ForwardServer') -> None:
# As soon as the timer is fired, the server can be stopped
# This means that:
# * If not connections are stablished, the server will be stopped
# * If no "keep_listening" is set, the server will not allow any new connections
logger.debug('New connection limit reached')
fs.timer = None
fs.can_stop = True
@ -196,10 +208,11 @@ class Handler(socketserver.BaseRequestHandler):
# server: ForwardServer
def handle(self) -> None:
self.server.status = ForwardState.TUNNEL_OPENING
if self.server.status == ForwardState.TUNNEL_LISTENING:
self.server.status = ForwardState.TUNNEL_OPENING # Only update state on first connection
# If server new connections processing are over time...
if self.server.stoppable:
if self.server.stoppable and not self.server.keep_listening:
self.server.status = ForwardState.TUNNEL_ERROR
logger.info('Rejected timedout connection')
self.request.close() # End connection without processing it
@ -217,11 +230,10 @@ class Handler(socketserver.BaseRequestHandler):
data = ssl_socket.recv(2)
if data != RESPONSE_OK:
data += ssl_socket.recv(128)
raise Exception(
f'Error received: {data.decode(errors="ignore")}'
) # Notify error
raise Exception(f'Error received: {data.decode(errors="ignore")}') # Notify error
# All is fine, now we can tunnel data
self.process(remote=ssl_socket)
except Exception as e:
logger.error(f'Error connecting to {self.server.remote!s}: {e!s}')
@ -258,10 +270,9 @@ class Handler(socketserver.BaseRequestHandler):
def _run(server: ForwardServer) -> None:
logger.debug(
'Starting forwarder: %s -> %s, timeout: %d',
'Starting forwarder: %s -> %s',
server.server_address,
server.remote,
server.timeout,
)
server.serve_forever()
logger.debug('Stoped forwarder %s -> %s', server.server_address, server.remote)
@ -273,14 +284,15 @@ def forward(
timeout: int = 0,
local_port: int = 0,
check_certificate=True,
keep_listening=True,
) -> ForwardServer:
fs = ForwardServer(
remote=remote,
ticket=ticket,
timeout=timeout,
local_port=local_port,
check_certificate=check_certificate,
keep_listening=keep_listening,
)
# Starts a new thread
threading.Thread(target=_run, args=(fs,)).start()
@ -295,18 +307,26 @@ if __name__ == "__main__":
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(levelname)s - %(message)s'
) # Basic log format, nice for syslog
formatter = logging.Formatter('%(levelname)s - %(message)s') # Basic log format, nice for syslog
handler.setFormatter(formatter)
log.addHandler(handler)
ticket = 'mffqg7q4s61fvx0ck2pe0zke6k0c5ipb34clhbkbs4dasb4g'
fs = forward(
('172.27.0.1', 7777),
('demoaslan.udsenterprise.com', 11443),
ticket,
local_port=49999,
local_port=0,
timeout=-20,
check_certificate=False,
)
print('Listening on port', fs.server_address)
import socket
# Open a socket to local fs.server_address and send some random data
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(fs.server_address)
s.sendall(b'Hello world!')
data = s.recv(1024)
print('Received', repr(data))
fs.stop()

View File

@ -7,9 +7,9 @@
<groupId>org.openuds.server</groupId>
<artifactId>guacamole-auth-uds</artifactId>
<packaging>jar</packaging>
<version>2.5.0</version>
<version>4.0.0</version>
<name>UDS Integration Extension for Apache Guacamole</name>
<url>https://github.com/dkmstr/openuds</url>
<url>https://github.com/VirtualCable/openuds</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -18,14 +18,13 @@
<build>
<plugins>
<!-- Compile using Java 1.8 -->
<!-- Compile using Java 11 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.3</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>11</source>
<target>11</target>
<compilerArgs>
<arg>-Xlint:all</arg>
<arg>-Werror</arg>
@ -38,7 +37,6 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>unpack-dependencies</id>
@ -70,15 +68,15 @@
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<version>1.1.1</version>
<scope>provided</scope>
<version>1.1.1</version>
</dependency>
<!-- Guacamole extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.2.0</version>
<version>1.5.1</version>
<scope>provided</scope>
</dependency>
@ -86,7 +84,7 @@
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
<version>5.1.0</version>
</dependency>
</dependencies>

View File

@ -55,7 +55,7 @@ public class UDSModule extends AbstractModule {
* If the guacamole.properties file cannot be read.
*/
public UDSModule() throws GuacamoleException {
this.environment = new LocalEnvironment();
this.environment = LocalEnvironment.getInstance();
}
@Override

View File

@ -30,13 +30,8 @@ package org.openuds.guacamole.config;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.properties.URIGuacamoleProperty;

636
server/.pylintrc Normal file
View File

@ -0,0 +1,636 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
enable-all-extensions=yes
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=lxml.etree
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=^\.#
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.11
# Discover python modules and packages in the file system subtree.
recursive=no
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=camelCase
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=camelCase
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=camelCase
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=camelCase
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=camelCase
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=camelCase
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=10
# Maximum number of attributes for a class (see R0902).
max-attributes=12
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=24
# Maximum number of locals for function / method body.
max-locals=24
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=32
# Maximum number of return / yield for function / method body.
max-returns=9
# Maximum number of statements in function / method body.
max-statements=64
# Minimum number of public methods for a class (see R0903).
min-public-methods=1
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=no
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
R0022,
broad-exception-raised,
invalid-name,
broad-except,
no-name-in-module, # Too many false positives... :(
import-error,
too-many-lines,
redefined-builtin,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=8
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the 'python-enchant' package.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members= .*.objects
.*.DoesNotExist.*
.+service,
.+osmanager,
ldap\..+,
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

View File

@ -1,21 +1,51 @@
# Broker (and common)
# Latest versions should work fine with master branch
Django
bitarray
numpy
html5lib
cryptography
python3-saml
six
dnspython
lxml
ovirt-engine-sdk-python
pycurl
matplotlib
pyOpenSSL
mysqlclient
python-ldap
paramiko
pyOpenSSL
pyrad
defusedxml
python-dateutil
requests
WeasyPrint
webencodings
xml-marshaller
cryptography
hypothesis
ipython
pyvmomi
PyJWT
pylibmc
gunicorn
python-dateutil
pywinrm
pywinrm[credssp]
whitenoise
setproctitle
openpyxl
boto3
uvicorn[standard]
numpy
pandas
xxhash
psutil
pyyaml
pyotp
qrcode
qrcode[pil]
art
# For tunnel
dnspython
aiohttp
uvloop

View File

@ -3,6 +3,7 @@
Settings file for uds server (Django)
'''
import os
import sys
import django
# calculated paths for django and the site
@ -57,20 +58,23 @@ TIME_ZONE = 'Europe/Madrid'
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en'
ugettext = lambda s: s
# Override for gettext so we can use the same syntax as in django
# and we can translate it later with our own function
def gettext(s):
return s
LANGUAGES = (
('es', ugettext('Spanish')),
('en', ugettext('English')),
('fr', ugettext('French')),
('de', ugettext('German')),
('pt', ugettext('Portuguese')),
('it', ugettext('Italian')),
('ar', ugettext('Arabic')),
('eu', ugettext('Basque')),
('ar', ugettext('Arabian')),
('ca', ugettext('Catalan')),
('zh-hans', ugettext('Chinese')),
('es', gettext('Spanish')),
('en', gettext('English')),
('fr', gettext('French')),
('de', gettext('German')),
('pt', gettext('Portuguese')),
('it', gettext('Italian')),
('ar', gettext('Arabic')),
('eu', gettext('Basque')),
('ar', gettext('Arabian')),
('ca', gettext('Catalan')),
('zh-hans', gettext('Chinese')),
)
LANGUAGE_COOKIE_NAME = 'uds_lang'
@ -159,10 +163,27 @@ FILE_UPLOAD_DIRECTORY_PERMISSIONS = 0o750
FILE_UPLOAD_MAX_MEMORY_SIZE = 512 * 1024 # 512 Kb
# Make this unique, and don't share it with anybody.
SECRET_KEY = 's5ky!7b5f#s35!e38xv%e-+iey6yi-#630x)kk3kk5_j8rie2*'
SECRET_KEY = 's5ky!7b5f#s35!e38xv%e-+iey6yi-#630x)kk3kk5_j8rie2*' # nosec: sample key, Remember to change it on production!!
# This is a very long string, an RSA KEY (this can be changed, but if u loose it, all encription will be lost)
RSA_KEY = '-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQC0qe1GlriQbHFYdKYRPBFDSS8Ne/TEKI2mtPKJf36XZTy6rIyH\nvUpT1gMScVjHjOISLNJQqktyv0G+ZGzLDmfkCUBev6JBlFwNeX3Dv/97Q0BsEzJX\noYHiDANUkuB30ukmGvG0sg1v4ccl+xs2Su6pFSc5bGINBcQ5tO0ZI6Q1nQIDAQAB\nAoGBAKA7Octqb+T/mQOX6ZXNjY38wXOXJb44LXHWeGnEnvUNf/Aci0L0epCidfUM\nfG33oKX4BMwwTVxHDrsa/HaXn0FZtbQeBVywZqMqWpkfL/Ho8XJ8Rsq8OfElrwek\nOCPXgxMzQYxoNHw8V97k5qhfupQ+h878BseN367xSyQ8plahAkEAuPgAi6aobwZ5\nFZhx/+6rmQ8sM8FOuzzm6bclrvfuRAUFa9+kMM2K48NAneAtLPphofqI8wDPCYgQ\nTl7O96GXVQJBAPoKtWIMuBHJXKCdUNOISmeEvEzJMPKduvyqnUYv17tM0JTV0uzO\nuDpJoNIwVPq5c3LJaORKeCZnt3dBrdH1FSkCQQC3DK+1hIvhvB0uUvxWlIL7aTmM\nSny47Y9zsc04N6JzbCiuVdeueGs/9eXHl6f9gBgI7eCD48QAocfJVygphqA1AkEA\nrvzZjcIK+9+pJHqUO0XxlFrPkQloaRK77uHUaW9IEjui6dZu4+2T/q7SjubmQgWR\nZy7Pap03UuFZA2wCoqJbaQJAUG0FVrnyUORUnMQvdDjAWps2sXoPvA8sbQY1W8dh\nR2k4TCFl2wD7LutvsdgdkiH0gWdh5tc1c4dRmSX1eQ27nA==\n-----END RSA PRIVATE KEY-----'
# Trusted cyphers
SECURE_CIPHERS = (
'TLS_AES_256_GCM_SHA384'
':TLS_CHACHA20_POLY1305_SHA256'
':TLS_AES_128_GCM_SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
':ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-CHACHA20-POLY1305'
':ECDHE-ECDSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES128-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305'
)
# Min TLS version
SECURE_MIN_TLS_VERSION = '1.2'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -234,7 +255,8 @@ WORKERSFILE = 'workers.log'
AUTHFILE = 'auth.log'
USEFILE = 'use.log'
TRACEFILE = 'trace.log'
LOGLEVEL = DEBUG and 'DEBUG' or 'INFO'
OPERATIONSFILE = 'operations.log'
LOGLEVEL = 'DEBUG' if DEBUG else 'INFO'
ROTATINGSIZE = 32 * 1024 * 1024 # 32 Megabytes before rotating files
LOGGING = {
@ -246,13 +268,21 @@ LOGGING = {
}
},
'formatters': {
'verbose': {
'database': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'simple': {
'format': '%(levelname)s %(asctime)s %(module)s %(funcName)s %(lineno)d %(message)s'
'format': '%(levelname)s %(asctime)s %(name)s:%(funcName)s %(lineno)d %(message)s'
},
'uds': {
'format': 'uds[%(process)-5s]: %(levelname)s %(asctime)s %(name)s:%(funcName)s %(lineno)d %(message)s'
},
'services': {
'format': 'uds-s[%(process)-5s]: %(levelname)s %(asctime)s %(name)s:%(funcName)s %(lineno)d %(message)s'
},
'workers': {
'format': 'uds-w[%(process)-5s]: %(levelname)s %(asctime)s %(name)s:%(funcName)s %(lineno)d %(message)s'
},
'database': {'format': '%(levelname)s %(asctime)s Database %(message)s'},
'auth': {'format': '%(asctime)s %(message)s'},
'use': {'format': '%(asctime)s %(message)s'},
'trace': {'format': '%(levelname)s %(asctime)s %(message)s'},
@ -262,9 +292,17 @@ LOGGING = {
'level': 'DEBUG',
'class': 'logging.NullHandler',
},
# Sample logging to syslog
#'file': {
# 'level': 'DEBUG',
# 'class': 'logging.handlers.SysLogHandler',
# 'formatter': 'uds',
# 'facility': 'local0',
# 'address': '/dev/log',
#},
'file': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'simple',
'filename': LOGDIR + '/' + LOGFILE,
'mode': 'a',
@ -275,7 +313,7 @@ LOGGING = {
'database': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'formatter': 'verbose',
'formatter': 'database',
'filename': LOGDIR + '/' + 'sql.log',
'mode': 'a',
'maxBytes': ROTATINGSIZE,
@ -284,7 +322,7 @@ LOGGING = {
},
'servicesFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'simple',
'filename': LOGDIR + '/' + SERVICESFILE,
'mode': 'a',
@ -294,7 +332,7 @@ LOGGING = {
},
'workersFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'simple',
'filename': LOGDIR + '/' + WORKERSFILE,
'mode': 'a',
@ -304,7 +342,7 @@ LOGGING = {
},
'authFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'auth',
'filename': LOGDIR + '/' + AUTHFILE,
'mode': 'a',
@ -314,7 +352,7 @@ LOGGING = {
},
'useFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'use',
'filename': LOGDIR + '/' + USEFILE,
'mode': 'a',
@ -324,7 +362,7 @@ LOGGING = {
},
'traceFile': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'trace',
'filename': LOGDIR + '/' + TRACEFILE,
'mode': 'a',
@ -332,6 +370,16 @@ LOGGING = {
'backupCount': 3,
'encoding': 'utf-8',
},
'operationsFile': {
'level': 'DEBUG',
'class': 'uds.core.util.log.UDSLogHandler',
'formatter': 'trace',
'filename': LOGDIR + '/' + OPERATIONSFILE,
'mode': 'a',
'maxBytes': ROTATINGSIZE,
'backupCount': 3,
'encoding': 'utf-8',
},
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
@ -374,6 +422,12 @@ LOGGING = {
'propagate': True,
'level': 'ERROR',
},
# Disable matplotlib (used by reports) logging (too verbose)
'matplotlib': {
'handlers': ['null'],
'propagate': True,
'level': 'ERROR',
},
'uds': {
'handlers': ['file'],
'level': LOGLEVEL,
@ -412,5 +466,11 @@ LOGGING = {
'level': 'INFO',
'propagate': False,
},
# Custom operations
'operationsLog': {
'handlers': ['operationsFile'],
'level': 'INFO',
'propagate': False,
},
},
}

View File

@ -33,7 +33,6 @@ import functools
import logging
from uds import models
from uds.core.managers import cryptoManager
from ...utils import rest
from ...fixtures import rest as rest_fixtures
@ -46,6 +45,7 @@ class GroupsTest(rest.test.RESTActorTestCase):
"""
Test users group rest api
"""
def setUp(self) -> None:
# Override number of items to create
rest.test.NUMBER_OF_ITEMS_TO_CREATE = 16
@ -66,7 +66,9 @@ class GroupsTest(rest.test.RESTActorTestCase):
for group in groups:
# Locate the group in the auth
dbgrp = self.auth.groups.get(name=group['name'])
self.assertTrue(rest.assertions.assertGroupIs(dbgrp, group, compare_uuid=True))
self.assertTrue(
rest.assertions.assertGroupIs(dbgrp, group, compare_uuid=True)
)
def test_groups_tableinfo(self) -> None:
url = f'authenticators/{self.auth.uuid}/groups/tableinfo'
@ -110,7 +112,6 @@ class GroupsTest(rest.test.RESTActorTestCase):
response = self.client.rest_get(f'{url}/invalid')
self.assertEqual(response.status_code, 404)
def test_group_create_edit(self) -> None:
url = f'authenticators/{self.auth.uuid}/groups'
# Normal group
@ -133,75 +134,12 @@ class GroupsTest(rest.test.RESTActorTestCase):
self.assertEqual(response.status_code, 400)
# Now a meta group, with some groups inside
groups = [self.simple_groups[0].uuid]
return
url = f'authenticators/{self.auth.uuid}/users'
user_dct: typing.Dict[str, typing.Any] = {
'name': 'test',
'real_name': 'test real name',
'comments': 'test comments',
'state': 'A',
'is_admin': True,
'staff_member': True,
'groups': [self.groups[0].uuid, self.groups[1].uuid],
}
# Now, will work
response = self.client.rest_put(
url,
user_dct,
content_type='application/json',
# groups = [self.simple_groups[0].uuid]
group_dct = rest_fixtures.createGroup(
meta=True, groups=[self.simple_groups[0].uuid, self.simple_groups[1].uuid]
)
# Get user from database and ensure values are correct
dbusr = self.auth.users.get(name=user_dct['name'])
self.assertEqual(user_dct['name'], dbusr.name)
self.assertEqual(user_dct['real_name'], dbusr.real_name)
self.assertEqual(user_dct['comments'], dbusr.comments)
self.assertEqual(user_dct['is_admin'], dbusr.is_admin)
self.assertEqual(user_dct['staff_member'], dbusr.staff_member)
self.assertEqual(user_dct['state'], dbusr.state)
self.assertEqual(user_dct['groups'], [i.uuid for i in dbusr.groups.all()])
self.assertEqual(response.status_code, 200)
# Returns nothing
# Now, will fail because name is already in use
response = self.client.rest_put(
url,
user_dct,
content_type='application/json',
)
self.assertEqual(response.status_code, 400)
# Modify saved user
user_dct['name'] = 'test2'
user_dct['real_name'] = 'test real name 2'
user_dct['comments'] = 'test comments 2'
user_dct['state'] = 'D'
user_dct['is_admin'] = False
user_dct['staff_member'] = False
user_dct['groups'] = [self.groups[2].uuid]
user_dct['id'] = dbusr.uuid
user_dct['password'] = 'test' # nosec: test password
user_dct['mfa_data'] = 'mfadata'
response = self.client.rest_put(
url,
user_dct,
content_type='application/json',
group_dct,
)
self.assertEqual(response.status_code, 200)
# Get user from database and ensure values are correct
dbusr = self.auth.users.get(name=user_dct['name'])
self.assertEqual(user_dct['name'], dbusr.name)
self.assertEqual(user_dct['real_name'], dbusr.real_name)
self.assertEqual(user_dct['comments'], dbusr.comments)
self.assertEqual(user_dct['is_admin'], dbusr.is_admin)
self.assertEqual(user_dct['staff_member'], dbusr.staff_member)
self.assertEqual(user_dct['state'], dbusr.state)
self.assertEqual(user_dct['groups'], [i.uuid for i in dbusr.groups.all()])
self.assertEqual(cryptoManager().checkHash(user_dct['password'], dbusr.password), True)

View File

@ -34,7 +34,10 @@ import typing
import logging
from uds import models
from uds.core.util import model
from uds.models import consts
from uds.models.account_usage import AccountUsage
from ...fixtures import services as services_fixtures
from ...utils.test import UDSTestCase
@ -52,9 +55,7 @@ class ModelAccountTest(UDSTestCase):
def setUp(self) -> None:
super().setUp()
self.user_services = services_fixtures.createCacheTestingUserServices(
NUM_USERSERVICES
)
self.user_services = services_fixtures.createCacheTestingUserServices(NUM_USERSERVICES)
def test_base(self) -> None:
acc = models.Account.objects.create(name='Test Account')
@ -62,7 +63,7 @@ class ModelAccountTest(UDSTestCase):
self.assertEqual(acc.name, 'Test Account')
self.assertIsInstance(acc.uuid, str)
self.assertEqual(acc.comments, '')
self.assertEqual(acc.time_mark, models.util.NEVER)
self.assertEqual(acc.time_mark, consts.NEVER)
# Ensures no ussage accounting is done
self.assertEqual(acc.usages.count(), 0)
@ -72,7 +73,7 @@ class ModelAccountTest(UDSTestCase):
acc.startUsageAccounting(self.user_services[0])
# Only one usage is createdm even with different accounters
self.assertEqual(acc.usages.count(), 1, 'loop {}'.format(i))
self.assertEqual(acc.usages.count(), 1, f'loop {i}')
# Now create one acconting with the same user service 32 times
# no usage is created because already created one for that user service
@ -80,7 +81,7 @@ class ModelAccountTest(UDSTestCase):
acc = models.Account.objects.create(name='Test Account')
acc.startUsageAccounting(self.user_services[0])
self.assertEqual(acc.usages.count(), 0, 'loop {}'.format(i))
self.assertEqual(acc.usages.count(), 0, f'loop {i}')
def test_start_single_many(self) -> None:
acc = models.Account.objects.create(name='Test Account')
@ -89,7 +90,7 @@ class ModelAccountTest(UDSTestCase):
acc.startUsageAccounting(self.user_services[i])
# Only one usage is createdm even with different accounters
self.assertEqual(acc.usages.count(), NUM_USERSERVICES, 'loop {}'.format(i))
self.assertEqual(acc.usages.count(), NUM_USERSERVICES, f'loop {i}'.format(i))
# Now create one acconting with the same user services 32 times
# no usage is created because already created one for that user service
@ -98,7 +99,7 @@ class ModelAccountTest(UDSTestCase):
for i in range(NUM_USERSERVICES):
acc.startUsageAccounting(self.user_services[i])
self.assertEqual(acc.usages.count(), 0, 'loop {}'.format(i))
self.assertEqual(acc.usages.count(), 0, f'loop {i}')
def test_start_multiple(self) -> None:
for i in range(NUM_USERSERVICES):
@ -109,9 +110,7 @@ class ModelAccountTest(UDSTestCase):
def test_end_single(self) -> None:
acc = models.Account.objects.create(name='Test Account')
for i in range(
32
): # will create 32 usages, because we close them all, even with one user service
for i in range(32): # will create 32 usages, because we close them all, even with one user service
acc.startUsageAccounting(self.user_services[i % NUM_USERSERVICES])
acc.stopUsageAccounting(self.user_services[i % NUM_USERSERVICES])
@ -120,15 +119,13 @@ class ModelAccountTest(UDSTestCase):
def test_end_single_many(self) -> None:
# Now create one acconting with the same user service 32 times
# no usage is created
for i in range(32):
for _ in range(32):
acc = models.Account.objects.create(name='Test Account')
for j in range(NUM_USERSERVICES):
acc.startUsageAccounting(self.user_services[j])
acc.stopUsageAccounting(self.user_services[j])
self.assertEqual(
acc.usages.count(), NUM_USERSERVICES
) # This acc will only have one usage
self.assertEqual(acc.usages.count(), NUM_USERSERVICES) # This acc will only have one usage
def test_account_usage(self) -> None:
acc = models.Account.objects.create(name='Test Account')
@ -145,7 +142,7 @@ class ModelAccountTest(UDSTestCase):
for i, usage in enumerate(AccountUsage.objects.all().order_by('id')):
self.assertEqual(usage.elapsed_seconds, 32 + i)
# With timemark to NEVER, we should get 0 in elapsed_seconds_timemark
usage.account.time_mark = models.util.NEVER
usage.account.time_mark = consts.NEVER
usage.account.save(update_fields=['time_mark'])
self.assertEqual(usage.elapsed_seconds_timemark, 0)
@ -161,24 +158,21 @@ class ModelAccountTest(UDSTestCase):
self.assertEqual(usage.elapsed_seconds_timemark, 32)
# With start or end to NEVER, we should get 0 in elapsed_seconds
usage.start = models.util.NEVER
usage.start = consts.NEVER
usage.save(update_fields=['start'])
self.assertEqual(usage.elapsed_seconds, 0)
usage.start = models.getSqlDatetime()
usage.end = models.util.NEVER
usage.start = model.getSqlDatetime()
usage.end = consts.NEVER
usage.save(update_fields=['start', 'end'])
self.assertEqual(usage.elapsed_seconds, 0)
# Now end is before start
usage.start = models.getSqlDatetime()
usage.start = model.getSqlDatetime()
usage.end = usage.start - datetime.timedelta(seconds=1)
usage.save(update_fields=['start', 'end'])
self.assertEqual(usage.elapsed_seconds, 0)
# Esnure elapsed and elapsed_timemark as strings
for i, usage in enumerate(AccountUsage.objects.all().order_by('id')):
self.assertIsInstance(usage.elapsed, str)
self.assertIsInstance(usage.elapsed_timemark, str)
self.assertIsInstance(str(usage), str)

View File

@ -32,13 +32,14 @@
"""
import time
from ...utils.test import UDSTestCase
from django.conf import settings
from uds.core.util.unique_id_generator import UniqueIDGenerator
from uds.core.util.unique_gid_generator import UniqueGIDGenerator
from uds.core.util.unique_mac_generator import UniqueMacGenerator
from uds.core.util.unique_name_generator import UniqueNameGenerator
from uds.models import getSqlDatetimeAsUnix
from uds.core.util.model import getSqlDatetimeAsUnix
from ...utils.test import UDSTestCase
NUM_THREADS = 8
@ -73,7 +74,7 @@ class UniqueIdTest(UDSTestCase):
self.assertEqual(self.uidGen.get(), 40)
def test_release_unique_id(self):
for x in range(100):
for _ in range(100):
self.uidGen.get()
self.assertEqual(self.uidGen.get(), 100)
@ -99,20 +100,20 @@ class UniqueIdTest(UDSTestCase):
self.assertEqual(self.uidGen.get(), i)
# from NUM to NUM*2-1 (both included) are still there, so we should get 200
self.assertEqual(self.uidGen.get(), NUM*2)
self.assertEqual(self.uidGen.get(), NUM*2+1)
self.assertEqual(self.uidGen.get(), NUM * 2)
self.assertEqual(self.uidGen.get(), NUM * 2 + 1)
def test_gid(self):
for x in range(100):
self.assertEqual(self.ugidGen.get(), 'uds{:08d}'.format(x))
self.assertEqual(self.ugidGen.get(), f'uds{x:08d}')
def test_gid_basename(self):
self.ugidGen.setBaseName('mar')
for x in range(100):
self.assertEqual(self.ugidGen.get(), 'mar{:08d}'.format(x))
self.assertEqual(self.ugidGen.get(), f'mar{x:08d}')
def test_mac(self):
start, end = TEST_MAC_RANGE.split('-')
start, end = TEST_MAC_RANGE.split('-') # pylint: disable=unused-variable
self.assertEqual(self.macGen.get(TEST_MAC_RANGE), start)
@ -150,7 +151,7 @@ class UniqueIdTest(UDSTestCase):
for x in range(20):
name = self.nameGen.get('test', length=length)
lst.append(name)
self.assertEqual(name, 'test{:0{width}d}'.format(num, width=length))
self.assertEqual(name, f'test{num:0{length}d}'.format(num, width=length))
num += 1
for x in lst:
@ -159,7 +160,7 @@ class UniqueIdTest(UDSTestCase):
self.assertEqual(self.nameGen.get('test', length=1), 'test0')
def test_name_full(self):
for x in range(10):
for _ in range(10):
self.nameGen.get('test', length=1)
with self.assertRaises(KeyError):

View File

@ -33,6 +33,7 @@ import typing
import datetime
from uds import models
from uds.core.util import model
from uds.core.environment import Environment
from uds.core.util import config
from uds.core.util.state import State
@ -57,13 +58,13 @@ class AssignedAndUnusedTest(UDSTestCase):
# Set now, should not be removed
count = models.UserService.objects.filter(state=State.REMOVABLE).count()
cleaner = AssignedAndUnused(Environment.getTempEnv())
# since_state = getSqlDatetime() - datetime.timedelta(seconds=cleaner.frecuency)
# since_state = util.getSqlDatetime() - datetime.timedelta(seconds=cleaner.frecuency)
cleaner.run()
self.assertEqual(models.UserService.objects.filter(state=State.REMOVABLE).count(), count)
# Set half the userServices to a long-ago state, should be removed
for i, us in enumerate(self.userServices):
if i%2 == 0:
us.state_date = models.getSqlDatetime() - datetime.timedelta(seconds=602)
us.state_date = model.getSqlDatetime() - datetime.timedelta(seconds=602)
us.save(update_fields=['state_date'])
cleaner.run()
self.assertEqual(models.UserService.objects.filter(state=State.REMOVABLE).count(), count + len(self.userServices)//2)

View File

@ -33,6 +33,7 @@ import datetime
import typing
from uds import models
from uds.core.util import model
from uds.core.environment import Environment
from uds.core.util import config
from uds.core.util.state import State
@ -75,7 +76,7 @@ class HangedCleanerTest(UDSTestCase):
us.state = State.USABLE
us.os_state = State.USABLE
us.state_date = models.getSqlDatetime() - datetime.timedelta(
us.state_date = model.getSqlDatetime() - datetime.timedelta(
seconds=MAX_INIT + 1
)
us.save(update_fields=['state', 'os_state', 'state_date'])

View File

@ -36,13 +36,14 @@ import random
from uds import models
from uds.core.util.stats import counters
from ...utils.test import UDSTestCase
from ...fixtures import stats_counters as fixtures_stats_counters
from uds.core.workers import stats_collector
from uds.core.environment import Environment
from uds.core.util import config
from ...utils.test import UDSTestCase
from ...fixtures import stats_counters as fixtures_stats_counters
START_DATE = datetime.datetime(2009, 12, 4, 0, 0, 0)
# Some random values,
@ -62,11 +63,13 @@ class StatsFunction:
def __call__(self, i: int, number_per_hour: int) -> int:
self.counter += 1
return self.counter * self.multiplier * 100 + random.randint(0, 100) # nosec: just testing values, lower 2 digits are random
return self.counter * self.multiplier * 100 + random.randint(
0, 100
) # nosec: just testing values, lower 2 digits are random
class StatsAcummulatorTest(UDSTestCase):
def setUp(self):
def setUp(self) -> None:
# In fact, real data will not be assigned to Userservices, but it's ok for testing
for pool_id in range(NUMBER_OF_POOLS):
fixtures_stats_counters.create_stats_interval_total(
@ -83,12 +86,10 @@ class StatsAcummulatorTest(UDSTestCase):
config.GlobalConfig.STATS_ACCUM_MAX_CHUNK_TIME.set(DAYS // 2 + 1)
stats_collector.StatsAccumulator.setup()
def test_stats_accumulator(self):
def test_stats_accumulator(self) -> None:
# Ensure first that we have correct number of base stats
base_stats = models.StatsCounters.objects.all()
total_base_stats = (
DAYS * 24 * NUMBER_PER_HOUR * NUMBER_OF_POOLS * len(COUNTERS_TYPES)
) # All stats
total_base_stats = DAYS * 24 * NUMBER_PER_HOUR * NUMBER_OF_POOLS * len(COUNTERS_TYPES) # All stats
self.assertEqual(base_stats.count(), total_base_stats)
optimizer = stats_collector.StatsAccumulator(Environment.getTempEnv())
@ -138,23 +139,16 @@ class StatsAcummulatorTest(UDSTestCase):
self.assertEqual(stat.v_count, len(d[stamp]))
# Recalculate sum of stats, now from StatsCountersAccum (dayly)
data_d: typing.Dict[
str, typing.Dict[int, typing.List[typing.Dict[str, int]]]
] = {}
data_d: typing.Dict[str, typing.Dict[int, typing.List[typing.Dict[str, int]]]] = {}
for i in hour_stats.order_by('owner_id', 'counter_type', 'stamp'):
stamp = (
i.stamp - (i.stamp % (3600 * 24)) + 3600 * 24
) # Round to day and to next day
d = data_d.setdefault(f'{i.owner_id:03d}{i.counter_type}', {})
d.setdefault(stamp, []).append(
{'sum': i.v_sum, 'count': i.v_count, 'max': i.v_max, 'min': i.v_min}
)
pass
stamp = i.stamp - (i.stamp % (3600 * 24)) + 3600 * 24 # Round to day and to next day
dd = data_d.setdefault(f'{i.owner_id:03d}{i.counter_type}', {})
dd.setdefault(stamp, []).append({'sum': i.v_sum, 'count': i.v_count, 'max': i.v_max, 'min': i.v_min})
for i in day_stats.order_by('owner_id', 'stamp'):
stamp = i.stamp # already rounded to day
d = data_d[f'{i.owner_id:03d}{i.counter_type}']
self.assertEqual(i.v_sum, sum([x['sum'] for x in d[stamp]]))
self.assertEqual(i.v_max, max([x['max'] for x in d[stamp]]))
self.assertEqual(i.v_min, min([x['min'] for x in d[stamp]]))
self.assertEqual(i.v_count, sum([x['count'] for x in d[stamp]]))
dd = data_d[f'{i.owner_id:03d}{i.counter_type}']
self.assertEqual(i.v_sum, sum(x['sum'] for x in dd[stamp]))
self.assertEqual(i.v_max, max(x['max'] for x in dd[stamp]))
self.assertEqual(i.v_min, min(x['min'] for x in dd[stamp]))
self.assertEqual(i.v_count, sum(x['count'] for x in dd[stamp]))

View File

@ -36,10 +36,8 @@ from uds.core.util import states
from uds.core.managers.crypto import CryptoManager
# Counters so we can reinvoke the same method and generate new data
glob = {
'user_id': 0,
'group_id': 0
}
glob = {'user_id': 0, 'group_id': 0}
def createAuthenticator(
authenticator: typing.Optional[models.Authenticator] = None,
@ -48,6 +46,7 @@ def createAuthenticator(
Creates a testing authenticator
"""
if authenticator is None:
# pylint: disable=import-outside-toplevel
from uds.auths.InternalDB.authenticator import InternalDBAuth
authenticator = models.Authenticator()
@ -74,10 +73,10 @@ def createUsers(
"""
users = [
authenticator.users.create(
name='user{}'.format(i),
password=CryptoManager().hash('user{}'.format(i)),
real_name='Real name {}'.format(i),
comments='User {}'.format(i),
name=f'user{i}',
password=CryptoManager().hash(f'user{i}'),
real_name=f'Real name {i}',
comments=f'User {i}',
staff_member=is_staff or is_admin,
is_admin=is_admin,
state=states.common.ACTIVE if enabled else states.common.BLOCKED,
@ -103,8 +102,8 @@ def createGroups(
"""
groups = [
authenticator.groups.create(
name='group{}'.format(i),
comments='Group {}'.format(i),
name=f'group{i}',
comments=f'Group {i}',
is_meta=False,
)
for i in range(glob['group_id'], glob['group_id'] + number_of_groups)
@ -123,8 +122,8 @@ def createMetaGroups(
"""
meta_groups = [
authenticator.groups.create(
name='meta-group{}'.format(i),
comments='Meta group {}'.format(i),
name=f'meta-group{i}',
comments=f'Meta group {i}',
is_meta=True,
meta_if_any=i % 2 == 0,
)
@ -136,9 +135,11 @@ def createMetaGroups(
groups = list(authenticator.groups.all())
if groups:
for meta in meta_groups:
for group in random.sample(groups, random.randint(1, len(groups)//2)): # nosec: testing only
for group in random.sample(
groups, random.randint(1, len(groups) // 2) # nosec: testing only
):
meta.groups.add(group)
glob['group_id'] += number_of_meta
return meta_groups

View File

@ -79,7 +79,7 @@ class EmailNotifierTest(UDSTestCase):
notifier.getInstance().notify(
'Group',
'Identificator',
messaging.NotificationLevel.CRITICAL,
messaging.LogLevel.CRITICAL,
'Test message cañón',
)

View File

@ -59,17 +59,17 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
self.client.enable_ipv4()
response = self.client.get('/', secure=False)
request = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
req = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
# session[AUTHORIZED_KEY] = False, not logged in
self.assertEqual(request.session.get(AUTHORIZED_KEY), False)
self.assertEqual(req.session.get(AUTHORIZED_KEY), False)
# Ensure ip, and ip_proxy are set and both are the same, 127.0.0.1
self.assertEqual(request.ip, '127.0.0.1')
self.assertEqual(request.ip_proxy, '127.0.0.1')
self.assertEqual(request.ip_version, 4)
self.assertEqual(req.ip, '127.0.0.1')
self.assertEqual(req.ip_proxy, '127.0.0.1')
self.assertEqual(req.ip_version, 4)
# Ensure user is not set
self.assertEqual(request.user, None)
self.assertEqual(req.user, None)
# And redirects to index
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('page.index'))
@ -81,17 +81,17 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
self.client.enable_ipv6()
response = self.client.get('/', secure=False)
request = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
req = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
# session[AUTHORIZED_KEY] = False, not logged in
self.assertEqual(request.session.get(AUTHORIZED_KEY), False)
self.assertEqual(req.session.get(AUTHORIZED_KEY), False)
# Ensure ip, and ip_proxy are set and both are the same,
self.assertEqual(request.ip, '::1')
self.assertEqual(request.ip_proxy, '::1')
self.assertEqual(request.ip_version, 6)
self.assertEqual(req.ip, '::1')
self.assertEqual(req.ip_proxy, '::1')
self.assertEqual(req.ip_version, 6)
# Ensure user is not set
self.assertEqual(request.user, None)
self.assertEqual(req.user, None)
# And redirects to index
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('page.index'))
@ -105,17 +105,17 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
user = self.login(as_admin=False)
response = self.client.get('/', secure=False)
request = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
req = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
# session[AUTHORIZED_KEY] = True, logged in
self.assertEqual(request.session.get(AUTHORIZED_KEY), True)
self.assertEqual(req.session.get(AUTHORIZED_KEY), True)
# Ensure ip, and ip_proxy are set and both are the same,
self.assertEqual(request.ip, '127.0.0.1')
self.assertEqual(request.ip_proxy, '127.0.0.1')
self.assertEqual(request.ip_version, 4)
self.assertEqual(req.ip, '127.0.0.1')
self.assertEqual(req.ip_proxy, '127.0.0.1')
self.assertEqual(req.ip_version, 4)
# Ensure user is correct
self.assertEqual(request.user.uuid, user.uuid)
self.assertEqual(req.user.uuid, user.uuid)
# And redirects to index
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('page.index'))
@ -129,44 +129,45 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
user = self.login(as_admin=False)
response = self.client.get('/', secure=False)
request = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
req = typing.cast('ExtendedHttpRequestWithUser', response.wsgi_request)
# session[AUTHORIZED_KEY] = True, logged in
self.assertEqual(request.session.get(AUTHORIZED_KEY), True)
self.assertEqual(req.session.get(AUTHORIZED_KEY), True)
# Ensure ip, and ip_proxy are set and both are the same,
self.assertEqual(request.ip, '::1')
self.assertEqual(request.ip_proxy, '::1')
self.assertEqual(request.ip_version, 6)
self.assertEqual(req.ip, '::1')
self.assertEqual(req.ip_proxy, '::1')
self.assertEqual(req.ip_version, 6)
# Ensure user is correct
self.assertEqual(request.user.uuid, user.uuid)
self.assertEqual(req.user.uuid, user.uuid)
# And redirects to index
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('page.index'))
def test_no_middleware(self) -> None:
# Ensure GlobalRequestMiddleware is not present
GlobalRequestMiddlewareTest.remove_middleware('uds.middleware.request.GlobalRequestMiddleware')
GlobalRequestMiddlewareTest.remove_middleware(
'uds.middleware.request.GlobalRequestMiddleware'
)
self.client.enable_ipv4()
response = self.client.get('/', secure=False)
request = response.wsgi_request
req = response.wsgi_request
# session[AUTHORIZED_KEY] is not present
self.assertEqual(AUTHORIZED_KEY in request.session, False)
self.assertEqual(AUTHORIZED_KEY in req.session, False)
# ip is not present, nor ip_proxy or ip_version
self.assertEqual(hasattr(request, 'ip'), False)
self.assertEqual(hasattr(request, 'ip_proxy'), False)
self.assertEqual(hasattr(request, 'ip_version'), False)
self.assertEqual(hasattr(req, 'ip'), False)
self.assertEqual(hasattr(req, 'ip_proxy'), False)
self.assertEqual(hasattr(req, 'ip_version'), False)
# Also, user is not present
self.assertEqual(hasattr(request, 'user'), False)
self.assertEqual(hasattr(req, 'user'), False)
# And redirects to index
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('page.index'))
def test_detect_ips_no_proxy(self) -> None:
req = mock.Mock()
# Use an ipv4 and an ipv6 address
@ -174,7 +175,7 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
req.META = {
'REMOTE_ADDR': ip,
}
request._fill_ips(req)
request._fill_ips(req) # pylint: disable=protected-access
self.assertEqual(req.ip, ip)
self.assertEqual(req.ip_proxy, ip)
self.assertEqual(req.ip_version, 4 if '.' in ip else 6)
@ -193,22 +194,34 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
'HTTP_X_FORWARDED_FOR': client_ip,
}
else:
req.META = {
'HTTP_X_FORWARDED_FOR': "{},{}".format(client_ip, proxy),
}
req.META = {'HTTP_X_FORWARDED_FOR': f'{client_ip},{proxy}'}
request._fill_ips(req)
self.assertEqual(req.ip, client_ip, "Failed for {}".format(req.META))
self.assertEqual(req.ip_proxy, client_ip, "Failed for {}".format(req.META))
self.assertEqual(req.ip_version, 4 if '.' in client_ip else 6, "Failed for {}".format(req.META))
request._fill_ips(req) # pylint: disable=protected-access
self.assertEqual(
req.ip, client_ip, "Failed for {}".format(req.META)
)
self.assertEqual(
req.ip_proxy, client_ip, "Failed for {}".format(req.META)
)
self.assertEqual(
req.ip_version,
4 if '.' in client_ip else 6,
"Failed for {}".format(req.META),
)
def test_detect_ips_proxy_chained(self) -> None:
config.GlobalConfig.BEHIND_PROXY.set(True)
req = mock.Mock()
# Use an ipv4 and an ipv6 address
for client_ip in ['192.168.128.128', '2001:db8:85a3:8d3:1319:8a2e:370:7348']:
for first_proxy in ['192.168.200.200', '2001:db8:85a3:8d3:1319:8a2e:370:7349']:
for second_proxy in ['192.168.201.201', '2001:db8:85a3:8d3:1319:8a2e:370:7350']:
for first_proxy in [
'192.168.200.200',
'2001:db8:85a3:8d3:1319:8a2e:370:7349',
]:
for second_proxy in [
'192.168.201.201',
'2001:db8:85a3:8d3:1319:8a2e:370:7350',
]:
for with_nginx in [True, False]:
x_forwarded_for = '{}, {}'.format(client_ip, first_proxy)
if with_nginx is False:
@ -218,11 +231,12 @@ class GlobalRequestMiddlewareTest(test.WEBTestCase):
}
else:
req.META = {
'HTTP_X_FORWARDED_FOR': "{}, {}".format(x_forwarded_for, second_proxy),
'HTTP_X_FORWARDED_FOR': "{}, {}".format(
x_forwarded_for, second_proxy
),
}
request._fill_ips(req)
self.assertEqual(req.ip, first_proxy)
self.assertEqual(req.ip_proxy, client_ip)
self.assertEqual(req.ip_version, 4 if '.' in first_proxy else 6)
self.assertEqual(req.ip_version, 4 if '.' in first_proxy else 6)

View File

@ -28,14 +28,11 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import logging
from django.urls import reverse
from uds.core.util import config
from uds.core.managers.crypto import CryptoManager
from uds.middleware.redirect import _NO_REDIRECT
from ..utils import test
@ -49,11 +46,11 @@ class RedirectMiddlewareTest(test.UDSTransactionTestCase):
"""
def test_redirect(self):
RedirectMiddlewareTest.add_middleware('uds.middleware.redirect.RedirectMiddleware')
config.GlobalConfig.REDIRECT_TO_HTTPS.set(True)
page = 'https://testserver' + reverse('page.index')
response = self.client.get('/', secure=False)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, 'https://testserver/')
# Try secure, will redirect to index
self.assertEqual(response.status_code, 301)
self.assertEqual(response.url, page)
# Try secure, will redirect to index, not absulute url
response = self.client.get('/', secure=True)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('page.index'))
@ -62,21 +59,7 @@ class RedirectMiddlewareTest(test.UDSTransactionTestCase):
for _ in range(32):
url = f'/{CryptoManager().randomString(32)}'
response = self.client.get(url, secure=False)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, f'https://testserver{url}')
self.assertEqual(response.status_code, 301)
self.assertEqual(response.url, page)
response = self.client.get(url, secure=True)
self.assertEqual(response.status_code, 404) # Not found
# These urls will never redirect:
for url in _NO_REDIRECT:
# Append some random string to avoid cache and make a 404 occur
url = f'/{url}{("/" + CryptoManager().randomString(32)) if "test" not in url else ""}'
response = self.client.get(url, secure=False)
# every url will return 404, except /uds/rest/client/test that will return 200 and wyse or servlet that will return 302
if url.startswith('/uds/rest/client/test'):
self.assertEqual(response.status_code, 200)
elif url.startswith('/wyse') or url.startswith('/servlet'):
self.assertEqual(response.status_code, 302)
else:
self.assertEqual(response.status_code, 404)

View File

@ -33,7 +33,6 @@ import logging
from django.urls import reverse
from uds.core.util import config
from uds.middleware.redirect import _NO_REDIRECT
from ..utils import test

View File

@ -36,13 +36,14 @@ import typing
from django.test import SimpleTestCase
from django.test.client import Client
# Not used, alloes "rest.test" or "rest.assertions"
from . import test
from . import assertions
from uds.REST.handlers import AUTH_TOKEN_HEADER
# Not used, allows "rest.test" or "rest.assertions"
from . import test # pylint: disable=unused-import
from . import assertions # pylint: disable=unused-import
from .. import generators
from uds.REST.handlers import AUTH_TOKEN_HEADER
# Calls REST login
def login(
@ -67,7 +68,7 @@ def login(
caller.assertEqual(
response.status_code,
expectedResponseCode,
'Login from {}'.format(errorMessage or caller.__class__.__name__),
f'Login from {errorMessage or caller.__class__.__name__}',
)
if response.status_code == 200:
@ -83,16 +84,18 @@ def logout(caller: SimpleTestCase, client: Client, auth_token: str) -> None:
**{AUTH_TOKEN_HEADER: auth_token}
)
caller.assertEqual(
response.status_code, 200, 'Logout Result: {}'.format(response.content)
response.status_code, 200, f'Logout Result: {response.content}'
)
caller.assertEqual(
response.json(), {'result': 'ok'}, 'Logout Result: {}'.format(response.content)
response.json(), {'result': 'ok'}, 'Logout Result: {response.content}'
)
# Rest related utils for fixtures
# Just a holder for a type, to indentify uuids
# pylint: disable=too-few-public-methods
class uuid_type:
pass
@ -100,7 +103,7 @@ class uuid_type:
RestFieldType = typing.Tuple[str, typing.Union[typing.Type, typing.Tuple[str, ...]]]
RestFieldReference = typing.Final[typing.List[RestFieldType]]
# pylint: disable=too-many-return-statements
def random_value(
field_type: typing.Union[typing.Type, typing.Tuple[str, ...]],
value: typing.Any = None,
@ -125,10 +128,15 @@ def random_value(
if field_type == typing.List[int]:
return [generators.random_int() for _ in range(generators.random_int(1, 10))]
if field_type == typing.List[bool]:
return [random.choice([True, False]) for _ in range(generators.random_int(1, 10))] # nosec
return [
random.choice([True, False]) for _ in range(generators.random_int(1, 10)) # nosec: test values
]
if field_type == typing.List[typing.Tuple[str, str]]:
return [(generators.random_utf8_string(), generators.random_utf8_string()) for _ in range(generators.random_int(1, 10))]
return [
(generators.random_utf8_string(), generators.random_utf8_string())
for _ in range(generators.random_int(1, 10))
]
return None
@ -139,13 +147,13 @@ class RestStruct:
def as_dict(self, **kwargs) -> typing.Dict[str, typing.Any]:
# Use kwargs to override values
res = {k: kwargs.get(k, getattr(self, k)) for k in self.__annotations__}
res = {k: kwargs.get(k, getattr(self, k)) for k in self.__annotations__} # pylint: disable=no-member
# Remove None values for optional fields
return {
k: v
for k, v in res.items()
if v is not None
or self.__annotations__[k]
or self.__annotations__[k] # pylint: disable=no-member
not in (
typing.Optional[str],
typing.Optional[bool],

View File

@ -32,8 +32,7 @@ import logging
import typing
from uds import models
from uds.core.auths.user import User as aUser
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from .. import ensure_data
@ -86,7 +85,7 @@ def assertUserIs(
# Compare password
if compare_password:
if not cryptoManager().checkHash(compare_to['password'], user.password):
if not CryptoManager().checkHash(compare_to['password'], user.password):
logger.info(
'User password do not match: %s != %s',
user.password,

View File

@ -28,19 +28,19 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
from uds import models
from uds.core.util import log
from uds.REST.handlers import AUTH_TOKEN_HEADER
from .. import test, generators, rest, constants
from ...fixtures import (
authenticators as authenticators_fixtures,
services as services_fixtures,
)
from uds.REST.handlers import AUTH_TOKEN_HEADER
NUMBER_OF_ITEMS_TO_CREATE = 4
@ -91,10 +91,10 @@ class RESTTestCase(test.UDSTransactionTestCase):
)
for user in self.users:
log.doLog(user, log.DEBUG, f'Debug Log for {user.name}')
log.doLog(user, log.INFO, f'Info Log for {user.name}')
log.doLog(user, log.WARNING, f'Warning Log for {user.name}')
log.doLog(user, log.ERROR, f'Error Log for {user.name}')
log.doLog(user, log.LogLevel.DEBUG, f'Debug Log for {user.name}')
log.doLog(user, log.LogLevel.INFO, f'Info Log for {user.name}')
log.doLog(user, log.LogLevel.WARNING, f'Warning Log for {user.name}')
log.doLog(user, log.LogLevel.ERROR, f'Error Log for {user.name}')
self.provider = services_fixtures.createProvider()

View File

@ -53,14 +53,9 @@ class UDSHttpResponse(HttpResponse):
super().__init__(content, *args, **kwargs)
self.content = content
def json(self) -> typing.Any:
return super().json() # type: ignore
class UDSClientMixin:
headers: typing.Dict[str, str] = {
'HTTP_USER_AGENT': 'Testing user agent',
}
uds_headers: typing.Dict[str, str]
ip_version: int = 4
def initialize(self):
@ -73,18 +68,21 @@ class UDSClientMixin:
'django.contrib.messages.middleware.MessageMiddleware',
'uds.middleware.request.GlobalRequestMiddleware',
]
self.uds_headers = {
'HTTP_USER_AGENT': 'Testing user agent',
}
# Update settings security options
settings.RSA_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcANi/08cnpn04\njKW/o2G1k4SIa6dJks8DmT4MQHOWqYC46YSIIPzqGoBPcvkbDSPSFBnByo3HhMY+\nk4JHc9SUwEmHSJWDCHjt7XSXX/ryqH0QQIJtSjk9Bc+GkOU24mnbITiw7ORjp7VN\nvgFdFhjVsZM/NjX/6Y9DoCPC1mGj0O9Dd4MfCsNwUxRhhR6LdrEnpRUSVW0Ksxbz\ncTfpQjdFr86+1BeUbzqN2HDcEGhvioj+0lGPXcOZoRNYU16H7kjLNP+o+rC7f/q/\nfoOYLzDSkmzePbcG+g0Hv7K7fuLus05ZWjupOmJA9hytB1BIF4p5f4ewl05Fx2Zj\nG2LneO2fAgMBAAECggEBANDimOnh2TkDceeMWx+OsAooC3E/zbEkjBudl3UoiNcn\nD0oCpkxeDeT0zpkgz/ZoTnd7kE0Y1e73WQc3JT5UcyXdQLMLLrIgDDnT+Jx1jB5z\n7XLN3UiJbblL2BOrZYbsCJf/fgU2l08rgBBVdJP+lAvps6YUAcd+6gDKfsnSpRhU\nWBHLZde7l6vUJ2OK9ZmHaghF5E8Xx918OSUKFJfGTYL5JLTb/scdl8vQse1quWC1\nk48PPXK10vOFvYWonQpRb2cOK/PPjPXPNWzcQyQY9D1iOeFvRyLqOXYE/ZY+qDe2\nHdPGrkl67yz01nzepkWWg/ZNbMXeZZyOnZm0aXtOxtkCgYEA/Qz3mescgwrt67yh\nFrbXjUqiVf2IpbNt88CUcbY0r1EdTA9OMtOtPYNvfpyRIRfDaZJ1zAdh3CZ2/hTm\ng+VUtseKnUDCi0xIBKX3V2O8sryWt2KStTnTo6JP0T47yXvmaRu5cutgoaD9SK+r\nN5vg1D2gNLmsT8uJh1Bl/yWGC4sCgYEA3pFGgAmiywsvmsddkI+LujoQVTiqkfFg\nMHHsJFOZlhYO83g49Q11pcQ70ukT6e89Ggwy///+z19p8jJ+wGqQWQLsM6eO1utg\nnJ8wMTwk8tOEm9MnWnnWhtG9KWcgkmwOVQiesJdWa1xOqsBKGchUkugmFycKNsiG\nHUbogbJ0OL0CgYBVLIcuxKdNKGGaxlwGVDbLdQKdJQBYncN1ly2f9K9ZD1loH4K3\nsu4N1W6y1Co5VFFO+KAzs4xp2HyW2xwX6xoPh6yNb53L2zombmKJhKWgF8A3K7Or\n0jH9UwXArUzcbZrJaC6MktNss85tJ8vepNYROkjxVkm8dgrtg89BCTVMLwKBgQCW\nSSh+uoL3cdUyQV63h4ZFOIHg2cOrin52F+bpXJ3/z2NHGa30IqOHTGtM7l+o/geX\nOBeT72tC4d2rUlduXEaeJDAUbRcxnnx9JayoAkG8ygDoK3uOR2kJXkTJ2T4QQPCo\nkIp/GaGcGxdviyo+IJyjGijmR1FJTrvotwG22iZKTQKBgQCIh50Dz0/rqZB4Om5g\nLLdZn1C8/lOR8hdK9WUyPHZfJKpQaDOlNdiy9x6xD6+uIQlbNsJhlDbOudHDurfI\nghGbJ1sy1FUloP+V3JAFS88zIwrddcGEso8YMFMCE1fH2/q35XGwZEnUq7ttDaxx\nHmTQ2w37WASIUgCl2GhM25np0Q==\n-----END PRIVATE KEY-----\n'
settings.CERTIFICATE = '-----BEGIN CERTIFICATE-----\nMIICzTCCAjYCCQCOUQEWpuEa3jANBgkqhkiG9w0BAQUFADCBqjELMAkGA1UEBhMC\nRVMxDzANBgNVBAgMBk1hZHJpZDEUMBIGA1UEBwwLQWxjb3Jjw4PCs24xHTAbBgNV\nBAoMFFZpcnR1YWwgQ2FibGUgUy5MLlUuMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEY\nMBYGA1UEAwwPQWRvbGZvIEfDg8KzbWV6MSUwIwYJKoZIhvcNAQkBFhZhZ29tZXpA\ndmlydHVhbGNhYmxlLmVzMB4XDTEyMDYyNTA0MjM0MloXDTEzMDYyNTA0MjM0Mlow\ngaoxCzAJBgNVBAYTAkVTMQ8wDQYDVQQIDAZNYWRyaWQxFDASBgNVBAcMC0FsY29y\nY8ODwrNuMR0wGwYDVQQKDBRWaXJ0dWFsIENhYmxlIFMuTC5VLjEUMBIGA1UECwwL\nRGV2ZWxvcG1lbnQxGDAWBgNVBAMMD0Fkb2xmbyBHw4PCs21lejElMCMGCSqGSIb3\nDQEJARYWYWdvbWV6QHZpcnR1YWxjYWJsZS5lczCBnzANBgkqhkiG9w0BAQEFAAOB\njQAwgYkCgYEA35iGyHS/GVdWk3n9kQ+wsCLR++jd9Vez/s407/natm8YDteKksA0\nMwIvDAX722blm8PUya2NOlnum8KdyUPDOq825XERDlsIA+sTd6lb1c7w44qZ/pb+\n68mhXoRx2VJsu//+zhBkaQ1/KcugeHa4WLRIH35YLxdQDxrXS1eQWccCAwEAATAN\nBgkqhkiG9w0BAQUFAAOBgQAk+fJPpY+XvUsxR2A4SaQ8TGnE2x4PtpwCrCVzKEU9\nW2ugdXvysxkHbib3+JdA6s+lJjHs5HiMZPo/ak8adEKke+d10EU5YcUaJRRUpStY\nqQHziaqOl5Hgi75Kjskq6+tCU0Iui+s9pBg0V6y1AQsCmH2xFs7t1oEOGRFVarfF\n4Q==\n-----END CERTIFICATE-----'
def add_header(self, name: str, value: str):
self.headers[name] = value
self.uds_headers[name] = value
def set_user_agent(self, user_agent: typing.Optional[str] = None):
user_agent = user_agent or ''
# Add 'HTTP_USER_AGENT' header
self.headers['HTTP_USER_AGENT'] = user_agent
self.uds_headers['HTTP_USER_AGENT'] = user_agent
def enable_ipv4(self):
self.ip_version = 4
@ -121,7 +119,7 @@ class UDSClient(UDSClientMixin, Client):
# Copy request dict
request = request.copy()
# Add headers
request.update(self.headers)
request.update(self.uds_headers)
return super().request(**request)
def get(self, *args, **kwargs) -> 'UDSHttpResponse':
@ -176,9 +174,10 @@ class UDSAsyncClient(UDSClientMixin, AsyncClient):
# Copy request dict
request = request.copy()
# Add headers
request.update(self.headers)
request.update(self.uds_headers)
return await super().request(**request)
# pylint: disable=invalid-overridden-method
async def get(self, *args, **kwargs) -> 'UDSHttpResponse':
self.append_remote_addr(kwargs)
return typing.cast('UDSHttpResponse', await super().get(*args, **kwargs))
@ -187,6 +186,7 @@ class UDSAsyncClient(UDSClientMixin, AsyncClient):
# compose url
return await self.get(self.compose_rest_url(method), *args, **kwargs)
# pylint: disable=invalid-overridden-method
async def post(self, *args, **kwargs) -> 'UDSHttpResponse':
self.append_remote_addr(kwargs)
return typing.cast('UDSHttpResponse', await super().post(*args, **kwargs))
@ -195,6 +195,7 @@ class UDSAsyncClient(UDSClientMixin, AsyncClient):
kwargs['content_type'] = kwargs.get('content_type', 'application/json')
return await self.post(self.compose_rest_url(method), *args, **kwargs)
# pylint: disable=invalid-overridden-method
async def put(self, *args, **kwargs) -> 'UDSHttpResponse':
kwargs['content_type'] = kwargs.get('content_type', 'application/json')
return typing.cast('UDSHttpResponse', await super().put(*args, **kwargs))
@ -203,6 +204,7 @@ class UDSAsyncClient(UDSClientMixin, AsyncClient):
kwargs['content_type'] = kwargs.get('content_type', 'application/json')
return await self.put(self.compose_rest_url(method), *args, **kwargs)
# pylint: disable=invalid-overridden-method
async def delete(self, *args, **kwargs) -> 'UDSHttpResponse':
self.append_remote_addr(kwargs)
return typing.cast('UDSHttpResponse', await super().delete(*args, **kwargs))
@ -247,6 +249,7 @@ class UDSTransactionTestCase(UDSTestCaseMixin, TransactionTestCase):
setupClass(cls)
# pylint: disable=unused-argument
def setupClass(
cls: typing.Union[typing.Type[UDSTestCase], typing.Type[UDSTransactionTestCase]]
) -> None:

View File

@ -33,6 +33,7 @@ import typing
from django.urls import reverse
from uds import models
from uds.core.util.config import GlobalConfig
from ...utils.web import test
@ -41,8 +42,6 @@ from ...fixtures import authenticators as fixtures_authenticators
if typing.TYPE_CHECKING:
from django.http import HttpResponse
from uds import models
class WebLoginLogoutTest(test.WEBTestCase):
"""
@ -98,7 +97,8 @@ class WebLoginLogoutTest(test.WEBTestCase):
# Ensures a couple of logs are created for every operation
# Except for root, that has no user associated on db
if up[0] is not root and up[1] is not rootpass: # root user is last one
self.assertEqual(models.Log.objects.count(), num * 4)
# 5 = 4 audit logs + 1 system log (auth.log)
self.assertEqual(models.Log.objects.count(), num * 5)
# Ensure web login for super user is disabled and that the root login fails
GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.set(False)
@ -122,7 +122,7 @@ class WebLoginLogoutTest(test.WEBTestCase):
response = self.do_login(user.name, user.name, user.manager.uuid)
self.assertInvalidLogin(response)
self.assertEqual(models.Log.objects.count(), 2)
self.assertEqual(models.Log.objects.count(), 4)
user = fixtures_authenticators.createUsers(
fixtures_authenticators.createAuthenticator(),
@ -132,7 +132,7 @@ class WebLoginLogoutTest(test.WEBTestCase):
response = self.do_login(user.name, user.name, user.manager.uuid, False)
self.assertInvalidLogin(response)
self.assertEqual(models.Log.objects.count(), 4)
self.assertEqual(models.Log.objects.count(), 8)
user = fixtures_authenticators.createUsers(
fixtures_authenticators.createAuthenticator(),
@ -142,7 +142,7 @@ class WebLoginLogoutTest(test.WEBTestCase):
response = self.do_login(user.name, user.name, user.manager.uuid)
self.assertInvalidLogin(response)
self.assertEqual(models.Log.objects.count(), 6)
self.assertEqual(models.Log.objects.count(), 12)
def test_login_invalid_user(self):
user = fixtures_authenticators.createUsers(
@ -153,7 +153,8 @@ class WebLoginLogoutTest(test.WEBTestCase):
self.assertInvalidLogin(response)
# Invalid password log & access denied, in auth and user log
self.assertEqual(models.Log.objects.count(), 4)
# + 2 system logs (auth.log), one for each failed login
self.assertEqual(models.Log.objects.count(), 6)
user = fixtures_authenticators.createUsers(
fixtures_authenticators.createAuthenticator(),
@ -163,7 +164,7 @@ class WebLoginLogoutTest(test.WEBTestCase):
response = self.do_login(user.name, 'wrong password', user.manager.uuid)
self.assertInvalidLogin(response)
self.assertEqual(models.Log.objects.count(), 8)
self.assertEqual(models.Log.objects.count(), 12)
user = fixtures_authenticators.createUsers(
fixtures_authenticators.createAuthenticator(),
@ -173,4 +174,4 @@ class WebLoginLogoutTest(test.WEBTestCase):
response = self.do_login(user.name, 'wrong password', user.manager.uuid)
self.assertInvalidLogin(response)
self.assertEqual(models.Log.objects.count(), 12)
self.assertEqual(models.Log.objects.count(), 18)

View File

@ -36,7 +36,6 @@ import traceback
from django import http
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
@ -92,7 +91,9 @@ class Dispatcher(View):
service = Dispatcher.services
full_path_lst: typing.List[str] = []
# Guess content type from content type header (post) or ".xxx" to method
content_type: str = request.META.get('CONTENT_TYPE', 'application/json').split(';')[0]
content_type: str = request.META.get('CONTENT_TYPE', 'application/json').split(
';'
)[0]
while path:
clean_path = path[0]
@ -112,13 +113,15 @@ class Dispatcher(View):
logger.debug("REST request: %s (%s)", full_path, content_type)
# Here, service points to the path and the value of '' is the handler
cls: typing.Optional[typing.Type[Handler]] = service[''] # Get "root" class, that is stored on
cls: typing.Optional[typing.Type[Handler]] = service[
''
] # Get "root" class, that is stored on
if not cls:
return http.HttpResponseNotFound(
'Method not found', content_type="text/plain"
)
processor = processors.available_processors_mime_dict .get(
processor = processors.available_processors_mime_dict.get(
content_type, processors.default_processor
)(request)
@ -144,9 +147,9 @@ class Dispatcher(View):
logger.debug('Path: %s', full_path)
logger.debug('Error: %s', e)
log.log_operation(handler, 500, log.ERROR)
log.logOperation(handler, 500, log.LogLevel.ERROR)
return http.HttpResponseServerError(
'Invalid parameters invoking {0}: {1}'.format(full_path, e),
f'Invalid parameters invoking {full_path}: {e}',
content_type="text/plain",
)
except AttributeError:
@ -154,17 +157,17 @@ class Dispatcher(View):
for n in ['get', 'post', 'put', 'delete']:
if hasattr(handler, n):
allowedMethods.append(n)
log.log_operation(handler, 405, log.ERROR)
log.logOperation(handler, 405, log.LogLevel.ERROR)
return http.HttpResponseNotAllowed(
allowedMethods, content_type="text/plain"
)
except AccessDenied:
log.log_operation(handler, 403, log.ERROR)
log.logOperation(handler, 403, log.LogLevel.ERROR)
return http.HttpResponseForbidden(
'access denied', content_type="text/plain"
)
except Exception:
log.log_operation(handler, 500, log.ERROR)
log.logOperation(handler, 500, log.LogLevel.ERROR)
logger.exception('error accessing attribute')
logger.debug('Getting attribute %s for %s', http_method, full_path)
return http.HttpResponseServerError(
@ -174,7 +177,7 @@ class Dispatcher(View):
# Invokes the handler's operation, add headers to response and returns
try:
response = operation()
if not handler.raw: # Raw handlers will return an HttpResponse Object
response = processor.getResponse(response)
# Set response headers
@ -182,33 +185,33 @@ class Dispatcher(View):
for k, val in handler.headers().items():
response[k] = val
log.log_operation(handler, response.status_code, log.INFO)
log.logOperation(handler, response.status_code, log.LogLevel.INFO)
return response
except RequestError as e:
log.log_operation(handler, 400, log.ERROR)
log.logOperation(handler, 400, log.LogLevel.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
except ResponseError as e:
log.log_operation(handler, 500, log.ERROR)
log.logOperation(handler, 500, log.LogLevel.ERROR)
return http.HttpResponseServerError(str(e), content_type="text/plain")
except NotSupportedError as e:
log.log_operation(handler, 501, log.ERROR)
log.logOperation(handler, 501, log.LogLevel.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
except AccessDenied as e:
log.log_operation(handler, 403, log.ERROR)
log.logOperation(handler, 403, log.LogLevel.ERROR)
return http.HttpResponseForbidden(str(e), content_type="text/plain")
except NotFound as e:
log.log_operation(handler, 404, log.ERROR)
log.logOperation(handler, 404, log.LogLevel.ERROR)
return http.HttpResponseNotFound(str(e), content_type="text/plain")
except HandlerError as e:
log.log_operation(handler, 500, log.ERROR)
log.logOperation(handler, 500, log.LogLevel.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
except Exception as e:
log.log_operation(handler, 500, log.ERROR)
log.logOperation(handler, 500, log.LogLevel.ERROR)
# Get ecxeption backtrace
trace_back = traceback.format_exc()
logger.error('Exception processing request: %s', full_path)
for i in trace_back.splitlines():
logger.error(f'* {i}')
logger.error('* %s', i)
return http.HttpResponseServerError(str(e), content_type="text/plain")
@ -249,9 +252,8 @@ class Dispatcher(View):
it register all subclases of Handler. (In fact, it looks for packages inside "methods" package, child of this)
"""
logger.info('Initializing REST Handlers')
# Our parent module "REST", because we are in "dispatcher"
modName = __name__[:__name__.rfind('.')]
modName = __name__[: __name__.rfind('.')]
# Register all subclasses of Handler
modfinder.dynamicLoadAndRegisterPackages(
@ -262,7 +264,5 @@ class Dispatcher(View):
packageName='methods',
)
return
Dispatcher.initialize()

View File

@ -40,7 +40,7 @@ from uds.core.util.config import GlobalConfig
from uds.core.auths.auth import getRootUser
from uds.core.util import net
from uds.models import Authenticator, User
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from .exceptions import AccessDenied
@ -51,7 +51,9 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__)
AUTH_TOKEN_HEADER: typing.Final[str] = 'HTTP_X_AUTH_TOKEN' # nosec: this is not a password
AUTH_TOKEN_HEADER: typing.Final[
str
] = 'HTTP_X_AUTH_TOKEN' # nosec: this is not a password
class Handler:
@ -102,9 +104,8 @@ class Handler:
method: str,
params: typing.MutableMapping[str, typing.Any],
*args: str,
**kwargs
**kwargs,
):
logger.debug(
'Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated
)
@ -112,9 +113,7 @@ class Handler:
self.needs_admin or self.needs_staff
) and not self.authenticated: # If needs_admin, must also be authenticated
raise Exception(
'class {} is not authenticated but has needs_admin or needs_staff set!!'.format(
self.__class__
)
f'class {self.__class__} is not authenticated but has needs_admin or needs_staff set!!'
)
self._request = request
@ -153,7 +152,6 @@ class Handler:
else:
self._user = User() # Empty user for non authenticated handlers
def headers(self) -> typing.Dict[str, str]:
"""
Returns the headers of the REST request (all)
@ -185,6 +183,27 @@ class Handler:
except Exception: # nosec: intentionally ingoring exception
pass # If not found, just ignore it
@property
def request(self) -> 'ExtendedHttpRequestWithUser':
"""
Returns the request object
"""
return self._request
@property
def params(self) -> typing.Any:
"""
Returns the params object
"""
return self._params
@property
def args(self) -> typing.Tuple[str, ...]:
"""
Returns the args object
"""
return self._args
# Auth related
def getAuthToken(self) -> typing.Optional[str]:
"""
@ -217,7 +236,9 @@ class Handler:
staff_member = True # Make admins also staff members :-)
# crypt password and convert to base64
passwd = codecs.encode(cryptoManager().symCrypt(password, scrambler), 'base64').decode()
passwd = codecs.encode(
CryptoManager().symCrypt(password, scrambler), 'base64'
).decode()
session['REST'] = {
'auth': id_auth,
@ -314,7 +335,7 @@ class Handler:
return net.contains(
GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True), self._request.ip
)
except Exception as e:
except Exception:
logger.warning(
'Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.',
GlobalConfig.ADMIN_TRUSTED_SOURCES.get(),
@ -368,4 +389,4 @@ class Handler:
for name in names:
if name in self._params:
return self._params[name]
return ''
return ''

View File

@ -32,15 +32,10 @@ Author: Adolfo Gómez, dkmaster at dkmon dot com
import typing
from uds import models
from uds.core.util.log import (
REST,
OWNER_TYPE_AUDIT,
DEBUG,
INFO,
WARNING,
ERROR,
CRITICAL,
)
# Import for REST using this module can access constants easily
# pylint: disable=unused-import
from uds.core.util.log import LogLevel, LogSource, doLog
if typing.TYPE_CHECKING:
from .handlers import Handler
@ -53,6 +48,7 @@ if typing.TYPE_CHECKING:
UUID_REPLACER = (
('providers', models.Provider),
('services', models.Service),
('servicespools', models.ServicePool),
('users', models.User),
('groups', models.Group),
)
@ -68,14 +64,14 @@ def replacePath(path: str) -> str:
uuid = path.split(f'/{type}/')[1].split('/')[0]
name = model.objects.get(uuid=uuid).name # type: ignore
path = path.replace(uuid, f'[{name}]')
except Exception: # nosec: intentionally broad exception
except Exception: # nosec: intentionally broad exception
pass
return path
def log_operation(
handler: typing.Optional['Handler'], response_code: int, level: int = INFO
def logOperation(
handler: typing.Optional['Handler'], response_code: int, level: LogLevel = LogLevel.INFO
):
"""
Logs a request
@ -83,7 +79,7 @@ def log_operation(
if not handler:
return # Nothing to log
path = handler._request.path
path = handler.request.path
# If a common request, and no error, we don't log it because it's useless and a waste of resources
if response_code < 400 and any(
@ -93,15 +89,13 @@ def log_operation(
path = replacePath(path)
username = handler._request.user.pretty_name if handler._request.user else 'Unknown'
# Global log is used without owner nor type
models.Log.objects.create(
owner_id=0,
owner_type=OWNER_TYPE_AUDIT,
created=models.getSqlDatetime(),
username = handler.request.user.pretty_name if handler.request.user else 'Unknown'
doLog(
None,
level=level,
source=REST,
data=f'{handler._request.ip} {username}: [{handler._request.method}/{response_code}] {path}'[
message=f'{handler.request.ip}[{username}]: [{handler.request.method}/{response_code}] {path}'[
:4096
],
source=LogSource.REST,
avoidDuplicates=False,
)

View File

@ -39,6 +39,7 @@ from uds.models import ActorToken
from uds.REST.exceptions import RequestError, NotFound
from uds.REST.model import ModelHandler, OK
from uds.core.util import permissions
from uds.core.util.log import LogLevel
logger = logging.getLogger(__name__)
@ -64,18 +65,18 @@ class ActorTokens(ModelHandler):
def item_as_dict(self, item: ActorToken) -> typing.Dict[str, typing.Any]:
return {
'id': item.token,
'name': _('Token isued by {} from {}').format(
'name': str(_('Token isued by {} from {}')).format(
item.username, item.hostname or item.ip
),
'stamp': item.stamp,
'username': item.username,
'ip': item.ip,
'host': '{} - {}'.format(item.ip, item.mac),
'host': f'{item.ip} - {item.mac}',
'hostname': item.hostname,
'pre_command': item.pre_command,
'post_command': item.post_command,
'runonce_command': item.runonce_command,
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
'log_level': LogLevel.fromActorLevel(item.log_level).name # ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
}
def delete(self) -> str:
@ -92,6 +93,6 @@ class ActorTokens(ModelHandler):
try:
self.model.objects.get(token=self._args[0]).delete()
except self.model.DoesNotExist:
raise NotFound('Element do not exists')
raise NotFound('Element do not exists') from None
return OK

View File

@ -33,23 +33,27 @@ import time
import logging
import typing
import functools
import enum
from uds.models import (
getSqlDatetimeAsUnix,
getSqlDatetime,
ActorToken,
UserService,
Service,
TicketStore,
)
from uds.core.util.model import getSqlDatetimeAsUnix, getSqlDatetime
# from uds.core import VERSION
from uds.core.managers import userServiceManager, cryptoManager
from uds.core.managers.user_service import UserServiceManager
from uds.core.managers.crypto import CryptoManager
from uds.core import osmanagers
from uds.core.util import log, certs
from uds.core.util import log, security
from uds.core.util.state import State
from uds.core.util.cache import Cache
from uds.core.util.config import GlobalConfig
from uds.core import exceptions
from uds.models.service import ServiceTokenAlias
from ..handlers import Handler
@ -70,12 +74,32 @@ UNMANAGED = 'unmanaged' # matches the definition of UDS Actors OFC
cache = Cache('actorv3')
class BlockAccess(Exception):
class BlockAccess(exceptions.UDSException):
pass
class NotifyActionType(enum.StrEnum):
LOGIN = 'login'
LOGOUT = 'logout'
DATA = 'data'
@staticmethod
def valid_names() -> typing.List[str]:
return [e.value for e in NotifyActionType]
# Helpers
def fixIdsList(idsList: typing.List[str]) -> typing.List[str]:
"""
Params:
idsList: List of ids to fix
Returns:
List of ids with both upper and lower case
Comment:
Due to database case sensitiveness, we need to check for both upper and lower case
"""
return list(set([i.upper() for i in idsList] + [i.lower() for i in idsList]))
@ -107,7 +131,7 @@ def clearIfSuccess(func: typing.Callable) -> typing.Callable:
result = func(
*args, **kwargs
) # If raises any exception, it will be raised and we will not clear the counter
clearFailedIp(_self._request)
clearFailedIp(_self._request) # pylint: disable=protected-access
return result
return wrapper
@ -133,7 +157,7 @@ class ActorV3Action(Handler):
@staticmethod
def setCommsUrl(userService: UserService, ip: str, port: int, secret: str):
userService.setCommsUrl('https://{}:{}/actor/{}'.format(ip, port, secret))
userService.setCommsUrl(f'https://{ip}:{port}/actor/{secret}')
def getUserService(self) -> UserService:
'''
@ -143,7 +167,7 @@ class ActorV3Action(Handler):
return UserService.objects.get(uuid=self._params['token'])
except UserService.DoesNotExist:
logger.error('User service not found (params: %s)', self._params)
raise BlockAccess()
raise BlockAccess() from None
def action(self) -> typing.MutableMapping[str, typing.Any]:
return ActorV3Action.actorResult(error='Base action invoked')
@ -162,6 +186,46 @@ class ActorV3Action(Handler):
raise AccessDenied('Access denied')
# Some helpers
def notifyService(self, action: NotifyActionType) -> None:
try:
# If unmanaged, use Service locator
service: 'services.Service' = Service.objects.get(token=self._params['token']).getInstance()
# We have a valid service, now we can make notifications
# Build the possible ids and make initial filter to match service
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
validId: typing.Optional[str] = service.getValidId(idsList)
is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-')
# Must be valid
if action in (NotifyActionType.LOGIN, NotifyActionType.LOGOUT):
if not validId: # For login/logout, we need a valid id
raise Exception()
# Notify Service that someone logged in/out
if action == NotifyActionType.LOGIN:
# Try to guess if this is a remote session
service.processLogin(validId, remote_login=is_remote)
elif action == NotifyActionType.LOGOUT:
service.processLogout(validId, remote_login=is_remote)
elif action == NotifyActionType.DATA:
service.notifyData(validId, self._params['data'])
else:
raise Exception('Invalid action')
# All right, service notified..
except Exception as e:
# Log error and continue
logger.error('Error notifying service: %s (%s)', e, self._params)
raise BlockAccess() from None
class Test(ActorV3Action):
"""
@ -176,9 +240,7 @@ class Test(ActorV3Action):
if self._params.get('type') == UNMANAGED:
Service.objects.get(token=self._params['token'])
else:
ActorToken.objects.get(
token=self._params['token']
) # Not assigned, because only needs check
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
clearFailedIp(self._request)
except Exception:
# Increase failed attempts
@ -201,6 +263,7 @@ class Register(ActorV3Action):
- run_once_command: comand to run just once after the actor is started. The actor will stop after this.
The command is responsible to restart the actor.
- log_level: log level for the actor
- custom: Custom actor data (i.e. cetificate and comms_url for LinxApps, maybe other for other services)
"""
@ -223,24 +286,30 @@ class Register(ActorV3Action):
actorToken.post_command = self._params['post_command']
actorToken.runonce_command = self._params['run_once_command']
actorToken.log_level = self._params['log_level']
if 'custom' in self._params:
actorToken.custom = self._params['certificate']
actorToken.stamp = getSqlDatetime()
actorToken.save()
logger.info('Registered actor %s', self._params)
except Exception: # Not found, create a new token
actorToken = ActorToken.objects.create(
username=self._user.pretty_name,
ip_from=self._request.ip,
ip=self._params['ip'],
ip_version=self._request.ip_version,
hostname=self._params['hostname'],
mac=self._params['mac'],
pre_command=self._params['pre_command'],
post_command=self._params['post_command'],
runonce_command=self._params['run_once_command'],
log_level=self._params['log_level'],
token=secrets.token_urlsafe(36),
stamp=getSqlDatetime(),
)
kwargs = {
'username': self._user.pretty_name,
'ip_from': self._request.ip,
'ip': self._params['ip'],
'ip_version': self._request.ip_version,
'hostname': self._params['hostname'],
'mac': self._params['mac'],
'pre_command': self._params['pre_command'],
'post_command': self._params['post_command'],
'runonce_command': self._params['run_once_command'],
'log_level': self._params['log_level'],
'token': secrets.token_urlsafe(36),
'stamp': getSqlDatetime(),
}
if 'custom' in self._params:
kwargs['custom'] = self._params['custom']
actorToken = ActorToken.objects.create(**kwargs)
return ActorV3Action.actorResult(actorToken.token)
@ -291,8 +360,13 @@ class Initialize(ActorV3Action):
# Managed machines will not use this field (will return None)
alias_token: typing.Optional[str] = None
initialization_result = (
lambda own_token, unique_id, os, alias_token: ActorV3Action.actorResult(
def initialization_result(
own_token: typing.Optional[str],
unique_id: typing.Optional[str],
os: typing.Any,
alias_token: typing.Optional[str],
) -> typing.MutableMapping[str, typing.Any]:
return ActorV3Action.actorResult(
{
'own_token': own_token,
'unique_id': unique_id,
@ -300,7 +374,7 @@ class Initialize(ActorV3Action):
'alias_token': alias_token,
}
)
)
try:
token = self._params['token']
# First, try to locate an user service providing this token.
@ -313,20 +387,16 @@ class Initialize(ActorV3Action):
# If not found an alias, try to locate on service table
# Not on alias token, try to locate on Service table
if not service:
if not service:
service = typing.cast('Service', Service.objects.get(token=token))
# Locate an userService that belongs to this service and which
# Build the possible ids and make initial filter to match service
idsList = [x['ip'] for x in self._params['id']] + [
x['mac'] for x in self._params['id']
][:10]
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
dbFilter = UserService.objects.filter(deployed_service__service=service)
else:
# If not service provided token, use actor tokens
ActorToken.objects.get(
token=token
) # Not assigned, because only needs check
ActorToken.objects.get(token=token) # Not assigned, because only needs check
# Build the possible ids and make initial filter to match ANY userservice with provided MAC
idsList = [i['mac'] for i in self._params['id'][:5]]
dbFilter = UserService.objects.all()
@ -356,17 +426,12 @@ class Initialize(ActorV3Action):
if service and not alias_token: # Is a service managed by UDS
# Create a new alias for it, and save
alias_token = (
cryptoManager().randomString(40)
) # fix alias with new token
alias_token = CryptoManager().randomString(40) # fix alias with new token
service.aliases.create(alias=alias_token)
return initialization_result(
userService.uuid, userService.unique_id, osData, alias_token
)
return initialization_result(userService.uuid, userService.unique_id, osData, alias_token)
except (ActorToken.DoesNotExist, Service.DoesNotExist):
raise BlockAccess()
raise BlockAccess() from None
class BaseReadyChange(ActorV3Action):
@ -415,10 +480,10 @@ class BaseReadyChange(ActorV3Action):
if osManager:
osManager.toReady(userService)
userServiceManager().notifyReadyFromOsManager(userService, '')
UserServiceManager().notifyReadyFromOsManager(userService, '')
# Generates a certificate and send it to client.
privateKey, cert, password = certs.selfSignedCert(self._params['ip'])
privateKey, cert, password = security.selfSignedCert(self._params['ip'])
# Store certificate with userService
userService.setProperty('cert', cert)
userService.setProperty('priv', privateKey)
@ -488,51 +553,7 @@ class Version(ActorV3Action):
return ActorV3Action.actorResult()
class LoginLogout(ActorV3Action):
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
def notifyService(self, isLogin: bool) -> None:
try:
# If unmanaged, use Service locator
service: 'services.Service' = Service.objects.get(
token=self._params['token']
).getInstance()
# We have a valid service, now we can make notifications
# Build the possible ids and make initial filter to match service
idsList = [x['ip'] for x in self._params['id']] + [
x['mac'] for x in self._params['id']
][:10]
# ensure idsLists has upper and lower versions for case sensitive databases
idsList = fixIdsList(idsList)
validId: typing.Optional[str] = service.getValidId(idsList)
# Must be valid
if not validId:
raise Exception()
# Recover Id Info from service and validId
# idInfo = service.recoverIdInfo(validId)
# Notify Service that someone logged in/out
is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-')
if isLogin:
# Try to guess if this is a remote session
service.processLogin(validId, remote_login=is_remote)
else:
service.processLogout(validId, remote_login=is_remote)
# All right, service notified..
except Exception as e:
# Log error and continue
logger.error('Error notifying service: %s (%s)', e, self._params)
raise BlockAccess()
class Login(LoginLogout):
class Login(ActorV3Action):
"""
Notifies user logged id
"""
@ -550,15 +571,9 @@ class Login(LoginLogout):
# }
@staticmethod
def process_login(
userService: UserService, username: str
) -> typing.Optional[osmanagers.OSManager]:
osManager: typing.Optional[
osmanagers.OSManager
] = userService.getOsManagerInstance()
if (
not userService.in_use
): # If already logged in, do not add a second login (windows does this i.e.)
def process_login(userService: UserService, username: str) -> typing.Optional[osmanagers.OSManager]:
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.)
osmanagers.OSManager.loggedIn(userService, username)
return osManager
@ -572,18 +587,14 @@ class Login(LoginLogout):
try:
userService: UserService = self.getUserService()
osManager = Login.process_login(
userService, self._params.get('username') or ''
)
osManager = Login.process_login(userService, self._params.get('username') or '')
maxIdle = osManager.maxIdle() if osManager else None
logger.debug('Max idle: %s', maxIdle)
ip, hostname = userService.getConnectionSource()
session_id = (
userService.initSession()
) # creates a session for every login requested
session_id = userService.initSession() # creates a session for every login requested
if osManager: # For os managed services, let's check if we honor deadline
if osManager.ignoreDeadLine():
@ -593,10 +604,12 @@ class Login(LoginLogout):
else: # For non os manager machines, process deadline as always
deadLine = userService.deployed_service.getDeadline()
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
except (
Exception
): # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
if isManaged:
raise
self.notifyService(isLogin=True)
self.notifyService(action=NotifyActionType.LOGIN)
return ActorV3Action.actorResult(
{
@ -609,7 +622,7 @@ class Login(LoginLogout):
)
class Logout(LoginLogout):
class Logout(ActorV3Action):
"""
Notifies user logged out
"""
@ -617,23 +630,17 @@ class Logout(LoginLogout):
name = 'logout'
@staticmethod
def process_logout(
userService: UserService, username: str, session_id: str
) -> None:
def process_logout(userService: UserService, username: str, session_id: str) -> None:
"""
This method is static so can be invoked from elsewhere
"""
osManager: typing.Optional[
osmanagers.OSManager
] = userService.getOsManagerInstance()
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
# Close session
# For compat, we have taken '' as "all sessions"
userService.closeSession(session_id)
if (
userService.in_use
): # If already logged out, do not add a second logout (windows does this i.e.)
if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.)
osmanagers.OSManager.loggedOut(userService, username)
if osManager:
if osManager.isRemovableOnLogout(userService):
@ -647,18 +654,18 @@ class Logout(LoginLogout):
logger.debug('Args: %s, Params: %s', self._args, self._params)
try:
userService: UserService = (
self.getUserService()
) # if not exists, will raise an error
userService: UserService = self.getUserService() # if not exists, will raise an error
Logout.process_logout(
userService,
self._params.get('username') or '',
self._params.get('session_id') or '',
)
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
except (
Exception
): # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
if isManaged:
raise
self.notifyService(isLogin=False) # Logout notification
self.notifyService(NotifyActionType.LOGOUT) # Logout notification
return ActorV3Action.actorResult(
'notified'
) # Result is that we have not processed the logout in fact, but notified the service
@ -679,9 +686,9 @@ class Log(ActorV3Action):
# Adjust loglevel to own, we start on 10000 for OTHER, and received is 0 for OTHER
log.doLog(
userService,
int(self._params['level']) + 10000,
log.LogLevel.fromInt(int(self._params['level']) + 10000),
self._params['message'],
log.ACTOR,
log.LogSource.ACTOR,
)
return ActorV3Action.actorResult('ok')
@ -699,16 +706,12 @@ class Ticket(ActorV3Action):
try:
# Simple check that token exists
ActorToken.objects.get(
token=self._params['token']
) # Not assigned, because only needs check
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
except ActorToken.DoesNotExist:
raise BlockAccess() # If too many blocks...
raise BlockAccess() from None # If too many blocks...
try:
return ActorV3Action.actorResult(
TicketStore.get(self._params['ticket'], invalidate=True)
)
return ActorV3Action.actorResult(TicketStore.get(self._params['ticket'], invalidate=True))
except TicketStore.DoesNotExist:
return ActorV3Action.actorResult(error='Invalid ticket')
@ -741,9 +744,7 @@ class Unmanaged(ActorV3Action):
# Build the possible ids and ask service if it recognizes any of it
# If not recognized, will generate anyway the certificate, but will not be saved
idsList = [x['ip'] for x in self._params['id']] + [
x['mac'] for x in self._params['id']
][:10]
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
validId: typing.Optional[str] = service.getValidId(idsList)
# ensure idsLists has upper and lower versions for case sensitive databases
@ -772,16 +773,12 @@ class Unmanaged(ActorV3Action):
# Try to infer the ip from the valid id (that could be an IP or a MAC)
ip: str
try:
ip = next(
x['ip']
for x in self._params['id']
if x['ip'] == validId or x['mac'] == validId
)
ip = next(x['ip'] for x in self._params['id'] if validId in (x['ip'], x['mac']))
except StopIteration:
ip = self._params['id'][0]['ip'] # Get first IP if no valid ip found
# Generates a certificate and send it to client.
privateKey, certificate, password = certs.selfSignedCert(ip)
privateKey, certificate, password = security.selfSignedCert(ip)
cert: typing.Dict[str, str] = {
'private_key': privateKey,
'server_certificate': certificate,
@ -817,21 +814,22 @@ class Notify(ActorV3Action):
def get(self) -> typing.MutableMapping[str, typing.Any]:
logger.debug('Args: %s, Params: %s', self._args, self._params)
if (
'action' not in self._params
or 'token' not in self._params
or self._params['action'] not in ('login', 'logout')
):
# Requested login or logout
raise RequestError('Invalid parameters')
try:
action = NotifyActionType(self._params['action'])
token = self._params['token'] # pylint: disable=unused-variable # Just to check it exists
except Exception as e:
# Requested login, logout or whatever
raise RequestError('Invalid parameters') from e
try:
# Check block manually
checkBlockedIp(self._request) # pylint: disable=protected-access
if 'action' == 'login':
if action == NotifyActionType.LOGIN:
Login.action(typing.cast(Login, self))
else:
elif action == NotifyActionType.LOGOUT:
Logout.action(typing.cast(Logout, self))
elif action == NotifyActionType.DATA:
self.notifyService(action)
return ActorV3Action.actorResult('ok')
except UserService.DoesNotExist:

View File

@ -50,7 +50,7 @@ from .users_groups import Users, Groups
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.db import models
from uds.core import Module
from uds.core.module import Module
logger = logging.getLogger(__name__)
@ -154,7 +154,7 @@ class Authenticators(ModelHandler):
raise Exception() # Not found
except Exception as e:
logger.info('Type not found: %s', e)
raise NotFound('type not found')
raise NotFound('type not found') from e
def item_as_dict(self, item: Authenticator) -> typing.Dict[str, typing.Any]:
type_ = item.getType()
@ -214,19 +214,16 @@ class Authenticators(ModelHandler):
if type_ == 'user':
return list(auth.searchUsers(term))[:limit]
else:
return list(auth.searchGroups(term))[:limit]
return list(auth.searchGroups(term))[:limit]
except Exception as e:
logger.exception('Too many results: %s', e)
return [{'id': _('Too many results...'), 'name': _('Refine your query')}]
# self.invalidResponseException('{}'.format(e))
def test(self, type_: str):
from uds.core.environment import Environment
authType = auths.factory().lookup(type_)
if not authType:
raise self.invalidRequestException('Invalid type: {}'.format(type_))
raise self.invalidRequestException(f'Invalid type: {type_}')
dct = self._params.copy()
dct['_request'] = self._request

View File

@ -39,7 +39,7 @@ from django.db import IntegrityError
from uds.models.calendar_rule import freqs, CalendarRule
from uds.models.util import getSqlDatetime
from uds.core.util.model import getSqlDatetime
from uds.core.util import permissions
from uds.core.util.model import processUuid
@ -59,7 +59,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
"""
@staticmethod
def ruleToDict(item: CalendarRule, perm: int):
def ruleToDict(item: CalendarRule, perm: int) -> typing.Dict[str, typing.Any]:
"""
Convert a calRule db item to a dict for a rest response
:param item: Rule item (db)
@ -80,18 +80,17 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
return retVal
def getItems(self, parent: 'Calendar', item: typing.Optional[str]):
def getItems(self, parent: 'Calendar', item: typing.Optional[str]) -> typing.Any:
# Check what kind of access do we have to parent provider
perm = permissions.getEffectivePermission(self._user, parent)
try:
if item is None:
return [CalendarRules.ruleToDict(k, perm) for k in parent.rules.all()]
else:
k = parent.rules.get(uuid=processUuid(item))
return CalendarRules.ruleToDict(k, perm)
except Exception:
k = parent.rules.get(uuid=processUuid(item))
return CalendarRules.ruleToDict(k, perm)
except Exception as e:
logger.exception('itemId %s', item)
raise self.invalidItemException()
raise self.invalidItemException() from e
def getFields(self, parent: 'Calendar') -> typing.List[typing.Any]:
return [
@ -144,12 +143,12 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
calRule.__dict__.update(fields)
calRule.save()
except CalendarRule.DoesNotExist:
raise self.invalidItemException()
except IntegrityError: # Duplicate key probably
raise RequestError(_('Element already exists (duplicate key error)'))
raise self.invalidItemException() from None
except IntegrityError as e: # Duplicate key probably
raise RequestError(_('Element already exists (duplicate key error)')) from e
except Exception as e:
logger.exception('Saving calendar')
raise RequestError('incorrect invocation to PUT: {0}'.format(e))
raise RequestError(f'incorrect invocation to PUT: {e}') from e
def deleteItem(self, parent: 'Calendar', item: str) -> None:
logger.debug('Deleting rule %s from %s', item, parent)
@ -158,9 +157,9 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
calRule.calendar.modified = getSqlDatetime()
calRule.calendar.save()
calRule.delete()
except Exception:
except Exception as e:
logger.exception('Exception')
raise self.invalidItemException()
raise self.invalidItemException() from e
def getTitle(self, parent: 'Calendar') -> str:
try:

View File

@ -39,7 +39,8 @@ from uds.REST import RequestError
from uds.models import TicketStore
from uds.models import User
from uds.web.util import errors
from uds.core.managers import cryptoManager, userServiceManager
from uds.core.managers.user_service import UserServiceManager
from uds.core.managers.crypto import CryptoManager
from uds.core.util.config import GlobalConfig
from uds.core.services.exceptions import ServiceNotReadyError
from uds.core import VERSION as UDS_VERSION, REQUIRED_CLIENT_VERSION
@ -53,8 +54,6 @@ logger = logging.getLogger(__name__)
CLIENT_VERSION = UDS_VERSION
# Enclosed methods under /client path
class Client(Handler):
"""
@ -91,9 +90,10 @@ class Client(Handler):
if errorCode != 0:
# Reformat error so it is better understood by users
# error += ' (code {0:04X})'.format(errorCode)
error = _(
'Your service is being created. Please, wait while we complete it'
) + ' ({}%)'.format(int(errorCode * 25))
error = (
_('Your service is being created. Please, wait while we complete it')
+ f' ({int(errorCode)*25}%)'
)
res['error'] = error
res['retryable'] = '1' if retryable else '0'
@ -111,8 +111,12 @@ class Client(Handler):
def process(self, ticket: str, scrambler: str) -> typing.Dict[str, typing.Any]:
userService: typing.Optional['UserService'] = None
hostname = self._params.get('hostname', '') # Or if hostname is not included...
version = self._params.get('version', '0.0.0')
srcIp = self._request.ip
if version < REQUIRED_CLIENT_VERSION:
return Client.result(error='Client version not supported.\n Please, upgrade it.')
# Ip is optional,
if GlobalConfig.HONOR_CLIENT_IP_NOTIFY.getBool() is True:
srcIp = self._params.get('ip', srcIp)
@ -140,7 +144,7 @@ class Client(Handler):
userServiceInstance,
transport,
transportInstance,
) = userServiceManager().getService(
) = UserServiceManager().getService(
self._request.user,
self._request.os,
self._request.ip,
@ -156,7 +160,7 @@ class Client(Handler):
transport,
transportInstance,
)
password = cryptoManager().symDecrpyt(data['password'], scrambler)
password = CryptoManager().symDecrpyt(data['password'], scrambler)
# userService.setConnectionSource(srcIp, hostname) # Store where we are accessing from so we can notify Service
if not ip:
@ -166,7 +170,7 @@ class Client(Handler):
if not transportInstance:
raise Exception('No transport instance!!!')
transport_script =transportInstance.getEncodedTransportScript(
transport_script = transportInstance.getEncodedTransportScript(
userService,
transport,
ip,
@ -188,12 +192,8 @@ class Client(Handler):
)
except ServiceNotReadyError as e:
# Refresh ticket and make this retrayable
TicketStore.revalidate(
ticket, 20
) # Retry will be in at most 5 seconds, so 20 is fine :)
return Client.result(
error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True
)
TicketStore.revalidate(ticket, 20) # Retry will be in at most 5 seconds, so 20 is fine :)
return Client.result(error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True)
except Exception as e:
logger.exception("Exception")
return Client.result(error=str(e))
@ -218,15 +218,20 @@ class Client(Handler):
{
'availableVersion': CLIENT_VERSION,
'requiredVersion': REQUIRED_CLIENT_VERSION,
'downloadUrl': self._request.build_absolute_uri(
reverse('page.client-download')
),
'downloadUrl': self._request.build_absolute_uri(reverse('page.client-download')),
}
)
return match(self._args,
error, # In case of error, raises RequestError
return match(
self._args,
error, # In case of error, raises RequestError
((), noargs), # No args, return version
(('test',), self.test), # Test request, returns "Correct"
(('<ticket>', '<crambler>',), self.process), # Process request, needs ticket and scrambler
(
(
'<ticket>',
'<crambler>',
),
self.process,
), # Process request, needs ticket and scrambler
)

View File

@ -43,14 +43,13 @@ logger = logging.getLogger(__name__)
class Config(Handler):
needs_admin = True # By default, staff is lower level needed
def get(self):
cfg: CfgConfig.Value
def get(self) -> typing.Any:
return CfgConfig.getConfigValues(self.is_admin())
def put(self):
def put(self) -> typing.Any:
for section, secDict in self._params.items():
for key, vals in secDict.items():
logger.info('Updating config value %s.%s to %s by %s', section, key, vals['value'], self._user.name)
CfgConfig.update(section, key, vals['value'])
return 'done'

View File

@ -37,8 +37,8 @@ from uds.core.util.request import ExtendedHttpRequestWithUser
from uds.REST import Handler
from uds.REST import RequestError
from uds.core.managers import userServiceManager
from uds.core.managers import cryptoManager
from uds.core.managers.user_service import UserServiceManager
from uds.core.managers.crypto import CryptoManager
from uds.core.services.exceptions import ServiceNotReadyError
from uds.core.util.rest.tools import match
from uds.web.util import errors, services
@ -76,7 +76,7 @@ class Connection(Handler):
error = errors.errorString(error)
error = str(error) # Ensure error is an string
if errorCode != 0:
error += ' (code {0:04X})'.format(errorCode)
error += f' (code {errorCode:04X})'
res['error'] = error
res['retryable'] = '1' if retryable else '0'
@ -100,10 +100,10 @@ class Connection(Handler):
(
ip,
userService,
iads,
trans,
_, # iads,
_, #trans,
itrans,
) = userServiceManager().getService( # pylint: disable=unused-variable
) = UserServiceManager().getService( # pylint: disable=unused-variable
self._user,
self._request.os,
self._request.ip,
@ -132,18 +132,18 @@ class Connection(Handler):
def script(self, idService: str, idTransport: str, scrambler: str, hostname: str) -> typing.Dict[str, typing.Any]:
try:
res = userServiceManager().getService(
res = UserServiceManager().getService(
self._user, self._request.os, self._request.ip, idService, idTransport
)
logger.debug('Res: %s', res)
(
ip,
userService,
userServiceInstance,
_, # userServiceInstance,
transport,
transportInstance,
) = res # pylint: disable=unused-variable
password = cryptoManager().symDecrpyt(self.getValue('password'), scrambler)
password = CryptoManager().symDecrpyt(self.getValue('password'), scrambler)
userService.setConnectionSource(
self._request.ip, hostname
@ -172,14 +172,14 @@ class Connection(Handler):
logger.exception("Exception")
return Connection.result(error=str(e))
def getTicketContent(self, ticketId: str) -> typing.Dict[str, typing.Any]:
return {} # TODO: use this for something?
def getTicketContent(self, ticketId: str) -> typing.Dict[str, typing.Any]: # pylint: disable=unused-argument
return {}
def getUdsLink(self, idService: str, idTransport: str) -> typing.Dict[str, typing.Any]:
# Returns the UDS link for the user & transport
self._request.user = self._user # type: ignore
self._request._cryptedpass = self._session['REST']['password'] # type: ignore
self._request._scrambler = self._request.META['HTTP_SCRAMBLER'] # type: ignore
setattr(self._request, '_cryptedpass', self._session['REST']['password']) # type: ignore # pylint: disable=protected-access
setattr(self._request, '_scrambler', self._request.META['HTTP_SCRAMBLER']) # type: ignore # pylint: disable=protected-access
linkInfo = services.enableService(
self._request, idService=idService, idTransport=idTransport
)

View File

@ -173,7 +173,7 @@ class MetaPools(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in Image.objects.all()
]
),
@ -188,7 +188,7 @@ class MetaPools(ModelHandler):
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in ServicePoolGroup.objects.all()
]
),

View File

@ -110,12 +110,12 @@ class MetaServicesPool(DetailHandler):
log.doLog(
parent,
log.INFO,
log.LogLevel.INFO,
("Added" if uuid is None else "Modified")
+ " meta pool member {}/{}/{} by {}".format(
pool.name, priority, enabled, self._user.pretty_name
),
log.ADMIN,
log.LogSource.ADMIN,
)
return self.success()
@ -128,7 +128,7 @@ class MetaServicesPool(DetailHandler):
member.delete()
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
class MetaAssignedService(DetailHandler):
@ -243,7 +243,7 @@ class MetaAssignedService(DetailHandler):
else:
raise self.invalidItemException(_('Item is not removable'))
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
# Only owner is allowed to change right now
def saveItem(self, parent: MetaPool, item: typing.Optional[str]):
@ -276,4 +276,4 @@ class MetaAssignedService(DetailHandler):
service.save()
# Log change
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)

View File

@ -35,11 +35,10 @@ import typing
from django.utils.translation import gettext_lazy as _, gettext
from uds.core.environment import Environment
from uds.models import Notifier, NotificationLevel
from uds.models import Notifier, LogLevel
from uds.core import messaging
from uds.core.ui import gui
from uds.core.util import permissions
from uds.core.managers import notifications
from uds.REST.model import ModelHandler
@ -86,7 +85,7 @@ class Notifiers(ModelHandler):
for field in [
{
'name': 'level',
'values': [gui.choiceItem(i[0], i[1]) for i in NotificationLevel.all()],
'values': [gui.choiceItem(i[0], i[1]) for i in LogLevel.interesting()],
'label': gettext('Level'),
'tooltip': gettext('Level of notifications'),
'type': gui.InputField.Types.CHOICE,

View File

@ -71,9 +71,9 @@ class AccessCalendars(DetailHandler):
return AccessCalendars.as_dict(
parent.calendarAccess.get(uuid=processUuid(item))
)
except Exception:
except Exception as e:
logger.exception('err: %s', item)
raise self.invalidItemException()
raise self.invalidItemException() from e
def getTitle(self, parent: 'ServicePool'):
return _('Access restrictions by calendar')
@ -96,8 +96,10 @@ class AccessCalendars(DetailHandler):
access: str = self._params['access'].upper()
if access not in (ALLOW, DENY):
raise Exception()
except Exception:
raise self.invalidRequestException(_('Invalid parameters on request'))
except Exception as e:
raise self.invalidRequestException(
_('Invalid parameters on request')
) from e
priority = int(self._params['priority'])
if uuid is not None:
@ -114,22 +116,17 @@ class AccessCalendars(DetailHandler):
log.doLog(
parent,
log.INFO,
"Added access calendar {}/{} by {}".format(
calendar.name, access, self._user.pretty_name
),
log.ADMIN,
log.LogLevel.INFO,
f'{"Added" if uuid is None else "Updated"} access calendar {calendar.name}/{access} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
def deleteItem(self, parent: 'ServicePool', item: str) -> None:
calendarAccess = parent.calendarAccess.get(uuid=processUuid(self._args[0]))
logStr = "Removed access calendar {} by {}".format(
calendarAccess.calendar.name, self._user.pretty_name
)
logStr = f'Removed access calendar {calendarAccess.calendar.name} by {self._user.pretty_name}'
calendarAccess.delete()
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
class ActionsCalendars(DetailHandler):
@ -167,8 +164,8 @@ class ActionsCalendars(DetailHandler):
]
i = parent.calendaraction_set.get(uuid=processUuid(item))
return ActionsCalendars.as_dict(i)
except Exception:
raise self.invalidItemException()
except Exception as e:
raise self.invalidItemException() from e
def getTitle(self, parent: 'ServicePool'):
return _('Scheduled actions')
@ -197,13 +194,10 @@ class ActionsCalendars(DetailHandler):
params = json.dumps(self._params['params'])
# logger.debug('Got parameters: {} {} {} {} ----> {}'.format(calendar, action, eventsOffset, atStart, params))
logStr = "Added scheduled action \"{},{},{},{},{}\" by {}".format(
calendar.name,
action,
eventsOffset,
atStart and 'Start' or 'End',
params,
self._user.pretty_name,
logStr = (
f'{"Added" if uuid is None else "Updated"} scheduled action '
f'{calendar.name},{action},{eventsOffset},{"start" if atStart else "end"},{params} '
f'by {self._user.pretty_name}'
)
if uuid is not None:
@ -225,39 +219,35 @@ class ActionsCalendars(DetailHandler):
params=params,
)
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
def deleteItem(self, parent: 'ServicePool', item: str) -> None:
calendarAction = CalendarAction.objects.get(uuid=processUuid(self._args[0]))
logStr = "Removed scheduled action \"{},{},{},{},{}\" by {}".format(
calendarAction.calendar.name,
calendarAction.action,
calendarAction.events_offset,
calendarAction.at_start and 'Start' or 'End',
calendarAction.params,
self._user.pretty_name,
logStr = (
f'Removed scheduled action "{calendarAction.calendar.name},'
f'{calendarAction.action},{calendarAction.events_offset},'
f'{calendarAction.at_start and "Start" or "End"},'
f'{calendarAction.params}" by {self._user.pretty_name}'
)
calendarAction.delete()
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
def execute(self, parent: 'ServicePool', item: str):
logger.debug('Launching action')
uuid = processUuid(item)
calendarAction: CalendarAction = CalendarAction.objects.get(uuid=uuid)
self.ensureAccess(calendarAction, permissions.PermissionType.MANAGEMENT)
logStr = "Launched scheduled action \"{},{},{},{},{}\" by {}".format(
calendarAction.calendar.name,
calendarAction.action,
calendarAction.events_offset,
calendarAction.at_start and 'Start' or 'End',
calendarAction.params,
self._user.pretty_name,
logStr = (
f'Launched scheduled action "{calendarAction.calendar.name},'
f'{calendarAction.action},{calendarAction.events_offset},'
f'{calendarAction.at_start and "Start" or "End"},'
f'{calendarAction.params}" by {self._user.pretty_name}'
)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
calendarAction.execute()
log.doLog(parent, log.INFO, logStr, log.ADMIN)
return self.success()

View File

@ -97,10 +97,10 @@ class Permissions(Handler):
{
'id': perm.uuid,
'type': kind,
'auth': entity.manager.uuid,
'auth_name': entity.manager.name,
'entity_id': entity.uuid,
'entity_name': entity.name,
'auth': entity.manager.uuid, # type: ignore
'auth_name': entity.manager.name, # type: ignore
'entity_id': entity.uuid, # type: ignore
'entity_name': entity.name, # type: ignore
'perm': perm.permission,
'perm_name': perm.permission_as_string,
}
@ -108,7 +108,7 @@ class Permissions(Handler):
return sorted(res, key=lambda v: v['auth_name'] + v['entity_name'])
def get(self):
def get(self) -> typing.Any:
"""
Processes get requests
"""

View File

@ -42,12 +42,10 @@ from uds.core import exceptions
from uds.core.util import log
from uds.core.util import permissions
from uds.core.util.model import processUuid
from uds.core.util.config import GlobalConfig
from uds.core.environment import Environment
from uds.core.ui.images import DEFAULT_THUMB_BASE64
from uds.core.ui import gui
from uds.core.util.state import State
from uds.core.module import Module
from uds.REST.model import DetailHandler
@ -128,9 +126,9 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
k = parent.services.get(uuid=processUuid(item))
val = Services.serviceToDict(k, perm, full=True)
return self.fillIntanceFields(k, val)
except Exception:
logger.exception('itemId %s', item)
raise self.invalidItemException()
except Exception as e:
logger.error('Error getting services for %s: %s', parent, e)
raise self.invalidItemException() from e
def getRowStyle(self, parent: 'Provider') -> typing.Dict[str, typing.Any]:
return {'field': 'maintenance_mode', 'prefix': 'row-maintenance-'}
@ -184,27 +182,27 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
service.save()
except models.Service.DoesNotExist:
raise self.invalidItemException()
except IntegrityError: # Duplicate key probably
raise self.invalidItemException() from None
except IntegrityError as e: # Duplicate key probably
if service and service.token and not item:
service.delete()
raise RequestError(
_(
'Service token seems to be in use by other service. Please, select a new one.'
)
)
raise RequestError(_('Element already exists (duplicate key error)'))
) from e
raise RequestError(_('Element already exists (duplicate key error)')) from e
except exceptions.ValidationError as e:
if (
not item and service
): # Only remove partially saved element if creating new (if editing, ignore this)
self._deleteIncompleteService(service)
raise RequestError(_('Input error: {0}'.format(e)))
raise RequestError(_('Input error: {0}'.format(e))) from e
except Exception as e:
if not item and service:
self._deleteIncompleteService(service)
logger.exception('Saving Service')
raise RequestError('incorrect invocation to PUT: {0}'.format(e))
raise RequestError('incorrect invocation to PUT: {0}'.format(e)) from e
def deleteItem(self, parent: 'Provider', item: str) -> None:
try:
@ -214,7 +212,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
return
except Exception:
logger.exception('Deleting service')
raise self.invalidItemException()
raise self.invalidItemException() from None
raise RequestError('Item has associated deployed services')
@ -284,7 +282,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
parentInstance = parent.getInstance()
serviceType = parentInstance.getServiceByType(forType)
if not serviceType:
raise self.invalidItemException('Gui for {} not found'.format(forType))
raise self.invalidItemException(f'Gui for {forType} not found')
service = serviceType(
Environment.getTempEnv(), parentInstance
@ -314,7 +312,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
except Exception as e:
logger.exception('getGui')
raise ResponseError(str(e))
raise ResponseError(str(e)) from e
def getLogs(self, parent: 'Provider', item: str) -> typing.List[typing.Any]:
try:
@ -322,7 +320,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
logger.debug('Getting logs for %s', item)
return log.getLogs(service)
except Exception:
raise self.invalidItemException()
raise self.invalidItemException() from None
def servicesPools(self, parent: 'Provider', item: str) -> typing.Any:
service = parent.services.get(uuid=processUuid(item))

View File

@ -93,7 +93,7 @@ class ServicesPoolGroups(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in Image.objects.all()
]
),

View File

@ -44,8 +44,9 @@ from uds.models import (
ServicePoolGroup,
Account,
User,
getSqlDatetime,
)
from uds.core.util.model import getSqlDatetime
from uds.models.calendar_action import (
CALENDAR_ACTION_INITIAL,
CALENDAR_ACTION_MAX,
@ -63,7 +64,7 @@ from uds.models.calendar_action import (
CALENDAR_ACTION_REMOVE_STUCK_USERSERVICES,
)
from uds.core.managers import userServiceManager
from uds.core.managers.user_service import UserServiceManager
from uds.core.ui.images import DEFAULT_THUMB_BASE64
from uds.core.util.state import State
from uds.core.util.model import processUuid
@ -157,9 +158,7 @@ class ServicesPools(ModelHandler):
def getItems(self, *args, **kwargs):
# Optimized query, due that there is a lot of info needed for theee
d = getSqlDatetime() - datetime.timedelta(
seconds=GlobalConfig.RESTRAINT_TIME.getInt()
)
d = getSqlDatetime() - datetime.timedelta(seconds=GlobalConfig.RESTRAINT_TIME.getInt())
return super().getItems(
overview=kwargs.get('overview', True),
query=(
@ -180,11 +179,7 @@ class ServicesPools(ModelHandler):
filter=~Q(userServices__state__in=State.INFO_STATES),
)
)
.annotate(
preparing_count=Count(
'userServices', filter=Q(userServices__state=State.PREPARING)
)
)
.annotate(preparing_count=Count('userServices', filter=Q(userServices__state=State.PREPARING)))
.annotate(
error_count=Count(
'userServices',
@ -225,25 +220,23 @@ class ServicesPools(ModelHandler):
state = item.state
if item.isInMaintenance():
state = State.MAINTENANCE
# This needs a lot of queries, and really does not shows anything important i think...
# elif userServiceManager().canInitiateServiceFromDeployedService(item) is False:
# This needs a lot of queries, and really does not apport anything important to the report
# elif UserServiceManager().canInitiateServiceFromDeployedService(item) is False:
# state = State.SLOWED_DOWN
val = {
'id': item.uuid,
'name': item.name,
'short_name': item.short_name,
'tags': [tag.tag for tag in item.tags.all()],
'parent': item.service.name,
'parent_type': item.service.data_type,
'parent': item.service.name, # type: ignore
'parent_type': item.service.data_type, # type: ignore
'comments': item.comments,
'state': state,
'thumb': item.image.thumb64
if item.image is not None
else DEFAULT_THUMB_BASE64,
'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64,
'account': item.account.name if item.account is not None else '',
'account_id': item.account.uuid if item.account is not None else None,
'service_id': item.service.uuid,
'provider_id': item.service.provider.uuid,
'service_id': item.service.uuid, # type: ignore
'provider_id': item.service.provider.uuid, # type: ignore
'image_id': item.image.uuid if item.image is not None else None,
'initial_srvs': item.initial_srvs,
'cache_l1_srvs': item.cache_l1_srvs,
@ -256,8 +249,7 @@ class ServicesPools(ModelHandler):
'ignores_unused': item.ignores_unused,
'fallbackAccess': item.fallbackAccess,
'meta_member': [
{'id': i.meta_pool.uuid, 'name': i.meta_pool.name}
for i in item.memberOfMeta.all()
{'id': i.meta_pool.uuid, 'name': i.meta_pool.name} for i in item.memberOfMeta.all()
],
'calendar_message': item.calendar_message,
}
@ -270,12 +262,8 @@ class ServicesPools(ModelHandler):
restrained = item.error_count >= GlobalConfig.RESTRAINT_COUNT.getInt() # type: ignore
usage_count = item.usage_count # type: ignore
else:
valid_count = item.userServices.exclude(
state__in=State.INFO_STATES
).count()
preparing_count = item.userServices.filter(
state=State.PREPARING
).count()
valid_count = item.userServices.exclude(state__in=State.INFO_STATES).count()
preparing_count = item.userServices.filter(state=State.PREPARING).count()
restrained = item.isRestrained()
usage_count = -1
@ -289,15 +277,13 @@ class ServicesPools(ModelHandler):
poolGroupThumb = item.servicesPoolGroup.image.thumb64
val['state'] = state
val['thumb'] = (
item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64
)
val['thumb'] = item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64
val['user_services_count'] = valid_count
val['user_services_in_preparation'] = preparing_count
val['tags'] = [tag.tag for tag in item.tags.all()]
val['restrained'] = restrained
val['permission'] = permissions.getEffectivePermission(self._user, item)
val['info'] = Services.serviceInfo(item.service)
val['info'] = Services.serviceInfo(item.service) # type: ignore
val['pool_group_id'] = poolGroupId
val['pool_group_name'] = poolGroupName
val['pool_group_thumb'] = poolGroupThumb
@ -313,9 +299,7 @@ class ServicesPools(ModelHandler):
# if OSManager.objects.count() < 1: # No os managers, can't create db
# raise ResponseError(gettext('Create at least one OS Manager before creating a new service pool'))
if Service.objects.count() < 1:
raise ResponseError(
gettext('Create at least a service before creating a new service pool')
)
raise ResponseError(gettext('Create at least a service before creating a new service pool'))
g = self.addDefaultFields([], ['name', 'short_name', 'comments', 'tags'])
@ -325,7 +309,7 @@ class ServicesPools(ModelHandler):
'values': [gui.choiceItem('', '')]
+ gui.sortedChoices(
[
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name)
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name) # type: ignore
for v in Service.objects.all()
]
),
@ -339,7 +323,7 @@ class ServicesPools(ModelHandler):
'name': 'osmanager_id',
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()]
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()] # type: ignore
),
'label': gettext('OS Manager'),
'tooltip': gettext('OS Manager used as base of this service pool'),
@ -362,9 +346,7 @@ class ServicesPools(ModelHandler):
'name': 'allow_users_reset',
'value': False,
'label': gettext('Allow reset by users'),
'tooltip': gettext(
'If active, the user will be allowed to reset the service'
),
'tooltip': gettext('If active, the user will be allowed to reset the service'),
'type': gui.InputField.Types.CHECKBOX,
'order': 112,
'tab': gettext('Advanced'),
@ -393,10 +375,7 @@ class ServicesPools(ModelHandler):
'name': 'image_id',
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in Image.objects.all()
]
[gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()] # type: ignore
),
'label': gettext('Associated Image'),
'tooltip': gettext('Image assocciated with this service'),
@ -409,14 +388,12 @@ class ServicesPools(ModelHandler):
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64)
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
for v in ServicePoolGroup.objects.all()
]
),
'label': gettext('Pool group'),
'tooltip': gettext(
'Pool group for this pool (for pool classify on display)'
),
'tooltip': gettext('Pool group for this pool (for pool classify on display)'),
'type': gui.InputField.Types.IMAGE_CHOICE,
'order': 121,
'tab': gettext('Display'),
@ -447,9 +424,7 @@ class ServicesPools(ModelHandler):
'value': '0',
'minValue': '0',
'label': gettext('Services to keep in cache'),
'tooltip': gettext(
'Services kept in cache for improved user service assignation'
),
'tooltip': gettext('Services kept in cache for improved user service assignation'),
'type': gui.InputField.Types.NUMERIC,
'order': 131,
'tab': gettext('Availability'),
@ -459,9 +434,7 @@ class ServicesPools(ModelHandler):
'value': '0',
'minValue': '0',
'label': gettext('Services to keep in L2 cache'),
'tooltip': gettext(
'Services kept in cache of level2 for improved service generation'
),
'tooltip': gettext('Services kept in cache of level2 for improved service generation'),
'type': gui.InputField.Types.NUMERIC,
'order': 132,
'tab': gettext('Availability'),
@ -482,9 +455,7 @@ class ServicesPools(ModelHandler):
'name': 'show_transports',
'value': True,
'label': gettext('Show transports'),
'tooltip': gettext(
'If active, alternative transports for user will be shown'
),
'tooltip': gettext('If active, alternative transports for user will be shown'),
'type': gui.InputField.Types.CHECKBOX,
'tab': gettext('Advanced'),
'order': 130,
@ -493,7 +464,7 @@ class ServicesPools(ModelHandler):
'name': 'account_id',
'values': [gui.choiceItem(-1, '')]
+ gui.sortedChoices(
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()] # type: ignore
),
'label': gettext('Accounting'),
'tooltip': gettext('Account associated to this service pool'),
@ -506,16 +477,15 @@ class ServicesPools(ModelHandler):
return g
def beforeSave(
self, fields: typing.Dict[str, typing.Any]
) -> None: # pylint: disable=too-many-branches,too-many-statements
# pylint: disable=too-many-statements
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None:
# logger.debug(self._params)
try:
try:
service = Service.objects.get(uuid=processUuid(fields['service_id']))
fields['service_id'] = service.id
except:
raise RequestError(gettext('Base service does not exist anymore'))
except Exception:
raise RequestError(gettext('Base service does not exist anymore')) from None
try:
serviceType = service.getType()
@ -527,9 +497,7 @@ class ServicesPools(ModelHandler):
self._params['allow_users_reset'] = False
if serviceType.needsManager is True:
osmanager = OSManager.objects.get(
uuid=processUuid(fields['osmanager_id'])
)
osmanager = OSManager.objects.get(uuid=processUuid(fields['osmanager_id']))
fields['osmanager_id'] = osmanager.id
else:
del fields['osmanager_id']
@ -556,19 +524,11 @@ class ServicesPools(ModelHandler):
fields['cache_l1_srvs'] = int(fields['cache_l1_srvs'])
if serviceType.maxDeployed != -1:
fields['max_srvs'] = min(
(fields['max_srvs'], serviceType.maxDeployed)
)
fields['initial_srvs'] = min(
fields['initial_srvs'], serviceType.maxDeployed
)
fields['cache_l1_srvs'] = min(
fields['cache_l1_srvs'], serviceType.maxDeployed
)
except Exception:
raise RequestError(gettext('This service requires an OS Manager'))
fields['max_srvs'] = min((fields['max_srvs'], serviceType.maxDeployed))
fields['initial_srvs'] = min(fields['initial_srvs'], serviceType.maxDeployed)
fields['cache_l1_srvs'] = min(fields['cache_l1_srvs'], serviceType.maxDeployed)
except Exception as e:
raise RequestError(gettext('This service requires an OS Manager')) from e
# If max < initial or cache_1 or cache_l2
fields['max_srvs'] = max(
@ -586,9 +546,7 @@ class ServicesPools(ModelHandler):
if accountId != '-1':
try:
fields['account_id'] = Account.objects.get(
uuid=processUuid(accountId)
).id
fields['account_id'] = Account.objects.get(uuid=processUuid(accountId)).id
except Exception:
logger.exception('Getting account ID')
@ -618,14 +576,14 @@ class ServicesPools(ModelHandler):
except (RequestError, ResponseError):
raise
except Exception as e:
raise RequestError(str(e))
raise RequestError(str(e)) from e
def afterSave(self, item: ServicePool) -> None:
if self._params.get('publish_on_save', False) is True:
try:
item.publish()
except Exception as e:
logger.error('Could not publish service pool %s: %s',item.name, e)
except Exception as e:
logger.error('Could not publish service pool %s: %s', item.name, e)
def deleteItem(self, item: ServicePool) -> None:
try:
@ -659,7 +617,7 @@ class ServicesPools(ModelHandler):
# Returns the action list based on current element, for calendar
def actionsList(self, item: ServicePool) -> typing.Any:
validActions: typing.Tuple[typing.Dict, ...] = ()
itemInfo = item.service.getType()
itemInfo = item.service.getType() # type: ignore
if itemInfo.usesCache is True:
validActions += (
CALENDAR_ACTION_INITIAL,
@ -679,7 +637,7 @@ class ServicesPools(ModelHandler):
CALENDAR_ACTION_DEL_ALL_TRANSPORTS,
CALENDAR_ACTION_ADD_GROUP,
CALENDAR_ACTION_DEL_GROUP,
CALENDAR_ACTION_DEL_ALL_GROUPS
CALENDAR_ACTION_DEL_ALL_GROUPS,
)
# Advanced actions
@ -691,7 +649,7 @@ class ServicesPools(ModelHandler):
return validActions
def listAssignables(self, item: ServicePool) -> typing.Any:
service = item.service.getInstance()
service = item.service.getInstance() # type: ignore
return [gui.choiceItem(i[0], i[1]) for i in service.listAssignables()]
def createFromAssignable(self, item: ServicePool) -> typing.Any:
@ -699,7 +657,7 @@ class ServicesPools(ModelHandler):
return self.invalidRequestException('Invalid parameters')
logger.debug('Creating from assignable: %s', self._params)
userServiceManager().createFromAssignable(
UserServiceManager().createFromAssignable(
item,
User.objects.get(uuid=processUuid(self._params['user_id'])),
self._params['assignable_id'],

View File

@ -77,8 +77,8 @@ class ServicesUsage(DetailHandler):
'friendly_name': item.friendly_name,
'owner': owner,
'owner_info': owner_info,
'service': item.deployed_service.service.name,
'service_id': item.deployed_service.service.uuid,
'service': item.deployed_service.service.name, # type: ignore
'service_id': item.deployed_service.service.uuid, # type: ignore
'pool': item.deployed_service.name,
'pool_id': item.deployed_service.uuid,
'ip': props.get('ip', _('unknown')),

View File

@ -37,6 +37,7 @@ import logging
import typing
from uds import models
from uds.core.util.model import getSqlDatetime
from uds.core.util.model import processUuid
from uds.core.util.stats import counters
@ -71,7 +72,7 @@ def getServicesPoolsCounters(
+ str(POINTS)
+ str(since_days)
)
to = models.getSqlDatetime()
to = getSqlDatetime()
since: datetime.datetime = to - datetime.timedelta(days=since_days)
cachedValue: typing.Optional[bytes] = cache.get(cacheKey)
@ -103,9 +104,9 @@ def getServicesPoolsCounters(
# return [{'stamp': since + datetime.timedelta(hours=i*10), 'value': i*i*counter_type//4} for i in range(300)]
return val
except:
logger.exception('exception')
raise ResponseError('can\'t create stats for objects!!!')
except Exception as e:
logger.exception('getServicesPoolsCounters')
raise ResponseError('can\'t create stats for objects!!!') from e
class System(Handler):

View File

@ -38,7 +38,7 @@ import typing
from uds.REST import Handler
from uds.REST import RequestError
from uds import models
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from uds.core.util.model import processUuid
from uds.core.util import tools
@ -66,7 +66,7 @@ VALID_PARAMS = (
)
# Enclosed methods under /actor path
# Enclosed methods under /tickets path
class Tickets(Handler):
"""
Processes tickets access requests.
@ -121,17 +121,25 @@ class Tickets(Handler):
raise RequestError('Invalid method')
try:
for i in ('authId', 'auth_id', 'authTag', 'auth_tag', 'auth', 'auth_name', 'authSmallName'):
for i in (
'authId',
'auth_id',
'authTag',
'auth_tag',
'auth',
'auth_name',
'authSmallName',
):
if i in self._params:
raise StopIteration
if 'username' in self._params and 'groups' in self._params:
raise StopIteration()
raise RequestError('Invalid parameters (no auth or username/groups)')
except StopIteration:
pass # All ok
pass # All ok
# Must be invoked as '/rest/ticket/create, with "username", ("authId" or "auth_id") or ("auth_tag" or "authSmallName" or "authTag"), "groups" (array) and optionally "time" (in seconds) as paramteres
def put(
self,
@ -171,7 +179,7 @@ class Tickets(Handler):
groupIds: typing.List[str] = []
for groupName in tools.as_list(self.getParam('groups')):
try:
groupIds.append(auth.groups.get(name=groupName).uuid)
groupIds.append(auth.groups.get(name=groupName).uuid or '')
except Exception:
logger.info(
'Group %s from ticket does not exists on auth %s, forced creation: %s',
@ -185,6 +193,7 @@ class Tickets(Handler):
name=groupName,
comments='Autocreated form ticket by using force paratemeter',
).uuid
or ''
)
if not groupIds: # No valid group in groups names
@ -244,7 +253,7 @@ class Tickets(Handler):
):
pool.assignedGroups.add(auth.groups.get(uuid=addGrp))
servicePoolId = 'F' + pool.uuid
servicePoolId = 'F' + pool.uuid # type: ignore
except models.Authenticator.DoesNotExist:
return Tickets.result(error='Authenticator does not exists')
@ -257,7 +266,7 @@ class Tickets(Handler):
data = {
'username': username,
'password': cryptoManager().encrypt(password),
'password': CryptoManager().encrypt(password),
'realname': realname,
'groups': groupIds,
'auth': auth.uuid,

View File

@ -34,7 +34,7 @@ import logging
import typing
from uds import models
from uds.core import managers
from uds.core.util.model import getSqlDatetimeAsUnix, getSqlDatetime
from uds.REST import Handler
from uds.REST import AccessDenied
from uds.core.auths.auth import isTrustedSource
@ -99,11 +99,11 @@ class TunnelTicket(Handler):
sent, recv = self._params['sent'], self._params['recv']
# Ensures extra exists...
extra = extra or {}
now = models.getSqlDatetimeAsUnix()
now = getSqlDatetimeAsUnix()
totalTime = now - extra.get('b', now - 1)
msg = f'User {user.name} stopped tunnel {extra.get("t", "")[:8]}... to {host}:{port}: u:{sent}/d:{recv}/t:{totalTime}.'
log.doLog(user.manager, log.INFO, msg)
log.doLog(userService, log.INFO, msg)
log.doLog(user.manager, log.LogLevel.INFO, msg)
log.doLog(userService, log.LogLevel.INFO, msg)
# Try to log Close event
try:
@ -131,8 +131,8 @@ class TunnelTicket(Handler):
tunnel=self._args[0],
)
msg = f'User {user.name} started tunnel {self._args[0][:8]}... to {host}:{port} from {self._args[1]}.'
log.doLog(user.manager, log.INFO, msg)
log.doLog(userService, log.INFO, msg)
log.doLog(user.manager, log.LogLevel.INFO, msg)
log.doLog(userService, log.LogLevel.INFO, msg)
# Generate new, notify only, ticket
notifyTicket = models.TicketStore.create_for_tunnel(
userService=userService,
@ -140,7 +140,7 @@ class TunnelTicket(Handler):
host=host,
extra={
't': self._args[0], # ticket
'b': models.getSqlDatetimeAsUnix(), # Begin time stamp
'b': getSqlDatetimeAsUnix(), # Begin time stamp
},
validity=MAX_SESSION_LENGTH,
)
@ -149,7 +149,7 @@ class TunnelTicket(Handler):
return data
except Exception as e:
logger.info('Ticket ignored: %s', e)
raise AccessDenied()
raise AccessDenied() from e
class TunnelRegister(Handler):
@ -159,7 +159,7 @@ class TunnelRegister(Handler):
def post(self) -> typing.MutableMapping[str, typing.Any]:
tunnelToken: models.TunnelToken
now = models.getSqlDatetimeAsUnix()
now = getSqlDatetimeAsUnix()
try:
# If already exists a token for this MAC, return it instead of creating a new one, and update the information...
tunnelToken = models.TunnelToken.objects.get(
@ -168,7 +168,7 @@ class TunnelRegister(Handler):
# Update parameters
tunnelToken.username = self._user.pretty_name
tunnelToken.ip_from = self._request.ip
tunnelToken.stamp = models.getSqlDatetime()
tunnelToken.stamp = getSqlDatetime()
tunnelToken.save()
except Exception:
try:
@ -178,7 +178,7 @@ class TunnelRegister(Handler):
ip=self._params['ip'],
hostname=self._params['hostname'],
token=secrets.token_urlsafe(36),
stamp=models.getSqlDatetime(),
stamp=getSqlDatetime(),
)
except Exception as e:
return {'result': '', 'stamp': now, 'error': str(e)}

View File

@ -58,7 +58,7 @@ class TunnelTokens(ModelHandler):
def item_as_dict(self, item: TunnelToken) -> typing.Dict[str, typing.Any]:
return {
'id': item.token,
'name': _('Token isued by {} from {}').format(item.username, item.ip),
'name': str(_('Token isued by {} from {}')).format(item.username, item.ip),
'stamp': item.stamp,
'username': item.username,
'ip': item.ip,
@ -80,6 +80,6 @@ class TunnelTokens(ModelHandler):
try:
self.model.objects.get(token=self._args[0]).delete()
except self.model.DoesNotExist:
raise NotFound('Element do not exists')
raise NotFound('Element do not exists') from None
return OK

View File

@ -39,7 +39,7 @@ from uds import models
from uds.core.util.state import State
from uds.core.util.model import processUuid
from uds.core.util import log, permissions
from uds.core.managers import userServiceManager
from uds.core.managers.user_service import UserServiceManager
from uds.REST.model import DetailHandler
from uds.REST import ResponseError
@ -125,9 +125,9 @@ class AssignedService(DetailHandler):
return AssignedService.itemToDict(
parent.assignedUserServices().get(processUuid(uuid=processUuid(item)))
)
except Exception:
except Exception as e:
logger.exception('getItems')
raise self.invalidItemException()
raise self.invalidItemException() from e
def getTitle(self, parent: models.ServicePool) -> str:
return _('Assigned services')
@ -164,8 +164,8 @@ class AssignedService(DetailHandler):
)
logger.debug('Getting logs for %s', userService)
return log.getLogs(userService)
except Exception:
raise self.invalidItemException()
except Exception as e:
raise self.invalidItemException() from e
# This is also used by CachedService, so we use "userServices" directly and is valid for both
def deleteItem(self, parent: models.ServicePool, item: str) -> None:
@ -173,20 +173,14 @@ class AssignedService(DetailHandler):
userService: models.UserService = parent.userServices.get(
uuid=processUuid(item)
)
except Exception:
except Exception as e:
logger.exception('deleteItem')
raise self.invalidItemException()
raise self.invalidItemException() from e
if userService.user:
logStr = 'Deleted assigned service {} to user {} by {}'.format(
userService.friendly_name,
userService.user.pretty_name,
self._user.pretty_name,
)
logStr = f'Deleted assigned service {userService.friendly_name} to user {userService.user.pretty_name} by {self._user.pretty_name}'
else:
logStr = 'Deleted cached service {} by {}'.format(
userService.friendly_name, self._user.pretty_name
)
logStr = f'Deleted cached service {userService.friendly_name} by {self._user.pretty_name}'
if userService.state in (State.USABLE, State.REMOVING):
userService.remove()
@ -197,7 +191,7 @@ class AssignedService(DetailHandler):
else:
raise self.invalidItemException(_('Item is not removable'))
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
# Only owner is allowed to change right now
def saveItem(self, parent: models.ServicePool, item: typing.Optional[str]) -> None:
@ -207,9 +201,7 @@ class AssignedService(DetailHandler):
userService = parent.userServices.get(uuid=processUuid(item))
user = models.User.objects.get(uuid=processUuid(fields['user_id']))
logStr = 'Changing ownership of service from {} to {} by {}'.format(
userService.user, user.pretty_name, self._user.pretty_name
)
logStr = f'Changed ownership of service {userService.friendly_name} from {userService.user} to {user.pretty_name} by {self._user.pretty_name}'
# If there is another service that has this same owner, raise an exception
if (
@ -220,20 +212,18 @@ class AssignedService(DetailHandler):
> 0
):
raise self.invalidResponseException(
'There is already another user service assigned to {}'.format(
user.pretty_name
)
f'There is already another user service assigned to {user.pretty_name}'
)
userService.user = user # type: ignore
userService.save()
# Log change
log.doLog(parent, log.INFO, logStr, log.ADMIN)
log.doLog(parent, log.LogLevel.INFO, logStr, log.LogSource.ADMIN)
def reset(self, parent: 'models.ServicePool', item: str) -> typing.Any:
userService = parent.userServices.get(uuid=processUuid(item))
userServiceManager().reset(userService)
UserServiceManager().reset(userService)
class CachedService(AssignedService):
@ -259,9 +249,9 @@ class CachedService(AssignedService):
uuid=processUuid(item)
)
return AssignedService.itemToDict(cachedService, True)
except Exception:
except Exception as e:
logger.exception('getItems')
raise self.invalidItemException()
raise self.invalidItemException() from e
def getTitle(self, parent: models.ServicePool) -> str:
return _('Cached services')
@ -290,7 +280,7 @@ class CachedService(AssignedService):
logger.debug('Getting logs for %s', item)
return log.getLogs(userService)
except Exception:
raise self.invalidItemException()
raise self.invalidItemException() from None
class Groups(DetailHandler):
@ -350,9 +340,9 @@ class Groups(DetailHandler):
parent.assignedGroups.add(group)
log.doLog(
parent,
log.INFO,
"Added group {} by {}".format(group.pretty_name, self._user.pretty_name),
log.ADMIN,
log.LogLevel.INFO,
f'Added group {group.pretty_name} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
def deleteItem(self, parent: models.ServicePool, item: str) -> None:
@ -360,9 +350,9 @@ class Groups(DetailHandler):
parent.assignedGroups.remove(group)
log.doLog(
parent,
log.INFO,
"Removed group {} by {}".format(group.pretty_name, self._user.pretty_name),
log.ADMIN,
log.LogLevel.INFO,
f'Removed group {group.pretty_name} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
@ -409,9 +399,9 @@ class Transports(DetailHandler):
parent.transports.add(transport)
log.doLog(
parent,
log.INFO,
"Added transport {} by {}".format(transport.name, self._user.pretty_name),
log.ADMIN,
log.LogLevel.INFO,
f'Added transport {transport.name} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
def deleteItem(self, parent: models.ServicePool, item: str) -> None:
@ -421,9 +411,9 @@ class Transports(DetailHandler):
parent.transports.remove(transport)
log.doLog(
parent,
log.INFO,
"Removed transport {} by {}".format(transport.name, self._user.pretty_name),
log.ADMIN,
log.LogLevel.INFO,
f'Removed transport {transport.name} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
@ -457,11 +447,9 @@ class Publications(DetailHandler):
log.doLog(
parent,
log.INFO,
"Initated publication v{} by {}".format(
parent.current_pub_revision, self._user.pretty_name
),
log.ADMIN,
log.LogLevel.INFO,
f'Initiated publication v{parent.current_pub_revision} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
return self.success()
@ -486,15 +474,13 @@ class Publications(DetailHandler):
ds = models.ServicePoolPublication.objects.get(uuid=processUuid(uuid))
ds.cancel()
except Exception as e:
raise ResponseError("{}".format(e))
raise ResponseError(str(e)) from e
log.doLog(
parent,
log.INFO,
"Canceled publication v{} by {}".format(
parent.current_pub_revision, self._user.pretty_name
),
log.ADMIN,
log.LogLevel.INFO,
f'Canceled publication v{parent.current_pub_revision} by {self._user.pretty_name}',
log.LogSource.ADMIN,
)
return self.success()

View File

@ -44,7 +44,7 @@ from uds.core.auths.user import User as aUser
from uds.core.util import log
from uds.core.util.model import processUuid
from uds.models import Authenticator, User, Group, ServicePool
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from uds.REST import RequestError
from uds.core.ui.images import DEFAULT_THUMB_BASE64
@ -77,7 +77,6 @@ def getPoolsForGroups(groups):
class Users(DetailHandler):
custom_methods = ['servicesPools', 'userServices', 'cleanRelated']
def getItems(self, parent: Authenticator, item: typing.Optional[str]):
@ -120,35 +119,34 @@ class Users(DetailHandler):
or _('User')
)
return values
else:
u = parent.users.get(uuid=processUuid(item))
res = model_to_dict(
u,
fields=(
'name',
'real_name',
'comments',
'state',
'staff_member',
'is_admin',
'last_access',
'parent',
'mfa_data',
),
)
res['id'] = u.uuid
res['role'] = (
res['staff_member']
and (res['is_admin'] and _('Admin') or _('Staff member'))
or _('User')
)
usr = aUser(u)
res['groups'] = [g.dbGroup().uuid for g in usr.groups()]
logger.debug('Item: %s', res)
return res
except Exception:
u = parent.users.get(uuid=processUuid(item))
res = model_to_dict(
u,
fields=(
'name',
'real_name',
'comments',
'state',
'staff_member',
'is_admin',
'last_access',
'parent',
'mfa_data',
),
)
res['id'] = u.uuid
res['role'] = (
res['staff_member']
and (res['is_admin'] and _('Admin') or _('Staff member'))
or _('User')
)
usr = aUser(u)
res['groups'] = [g.dbGroup().uuid for g in usr.groups()]
logger.debug('Item: %s', res)
return res
except Exception as e:
logger.exception('En users')
raise self.invalidItemException()
raise self.invalidItemException() from e
def getTitle(self, parent):
try:
@ -191,7 +189,7 @@ class Users(DetailHandler):
try:
user = parent.users.get(uuid=processUuid(item))
except Exception:
raise self.invalidItemException()
raise self.invalidItemException() from None
return log.getLogs(user)
@ -210,7 +208,7 @@ class Users(DetailHandler):
if 'password' in self._params:
valid_fields.append('password')
self._params['password'] = cryptoManager().hash(self._params['password'])
self._params['password'] = CryptoManager().hash(self._params['password'])
if 'mfa_data' in self._params:
valid_fields.append('mfa_data')
@ -247,18 +245,18 @@ class Users(DetailHandler):
if g.is_meta is False
)
except User.DoesNotExist:
raise self.invalidItemException()
raise self.invalidItemException() from None
except IntegrityError: # Duplicate key probably
raise RequestError(_('User already exists (duplicate key error)'))
raise RequestError(_('User already exists (duplicate key error)')) from None
except AuthenticatorException as e:
raise RequestError(str(e))
raise RequestError(str(e)) from e
except ValidationError as e:
raise RequestError(str(e.message))
except RequestError:
raise
except Exception:
raise RequestError(str(e.message)) from e
except RequestError: # pylint: disable=try-except-raise
raise # Re-raise
except Exception as e:
logger.exception('Saving user')
raise self.invalidRequestException()
raise self.invalidRequestException() from e
return self.getItems(parent, user.uuid)
@ -266,9 +264,12 @@ class Users(DetailHandler):
try:
user = parent.users.get(uuid=processUuid(item))
if not self._user.is_admin and (user.is_admin or user.staff_member):
logger.warn('Removal of user {} denied due to insufficients rights')
logger.warning(
'Removal of user %s denied due to insufficients rights',
user.pretty_name,
)
raise self.invalidItemException(
'Removal of user {} denied due to insufficients rights'
f'Removal of user {user.pretty_name} denied due to insufficients rights'
)
assignedUserService: 'UserService'
@ -285,9 +286,9 @@ class Users(DetailHandler):
logger.exception('Saving user on removing error')
user.delete()
except Exception:
except Exception as e:
logger.exception('Removing user')
raise self.invalidItemException()
raise self.invalidItemException() from e
return 'deleted'
@ -325,7 +326,7 @@ class Users(DetailHandler):
res.append(v)
return res
def cleanRelated(self, parent: Authenticator, item: str) -> typing.Dict:
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
@ -334,7 +335,6 @@ class Users(DetailHandler):
class Groups(DetailHandler):
custom_methods = ['servicesPools', 'users']
def getItems(self, parent: Authenticator, item: typing.Optional[str]):
@ -364,14 +364,14 @@ class Groups(DetailHandler):
if multi:
return res
if not i:
raise # Invalid item
raise Exception('Item not found')
# Add pools field if 1 item only
result = res[0]
result['pools'] = [v.uuid for v in getPoolsForGroups([i])]
return result
except Exception:
except Exception as e:
logger.exception('REST groups')
raise self.invalidItemException()
raise self.invalidItemException() from e
def getTitle(self, parent: Authenticator) -> str:
try:
@ -409,12 +409,12 @@ class Groups(DetailHandler):
}
types = [
{
'name': tDct[t]['name'],
'type': t,
'description': tDct[t]['description'],
'name': v['name'],
'type': k,
'description': v['description'],
'icon': '',
}
for t in tDct
for k, v in tDct.items()
]
if forType is None:
@ -423,7 +423,7 @@ class Groups(DetailHandler):
try:
return next(filter(lambda x: x['type'] == forType, types))
except Exception:
raise self.invalidRequestException()
raise self.invalidRequestException() from None
def saveItem(self, parent: Authenticator, item: typing.Optional[str]) -> None:
group = None # Avoid warning on reference before assignment
@ -467,7 +467,11 @@ class Groups(DetailHandler):
if is_meta:
# Do not allow to add meta groups to meta groups
group.groups.set(i for i in parent.groups.filter(uuid__in=self._params['groups']) if i.is_meta is False)
group.groups.set(
i
for i in parent.groups.filter(uuid__in=self._params['groups'])
if i.is_meta is False
)
if pools:
# Update pools
@ -475,16 +479,16 @@ class Groups(DetailHandler):
group.save()
except Group.DoesNotExist:
raise self.invalidItemException()
raise self.invalidItemException() from None
except IntegrityError: # Duplicate key probably
raise RequestError(_('User already exists (duplicate key error)'))
raise RequestError(_('User already exists (duplicate key error)')) from None
except AuthenticatorException as e:
raise RequestError(str(e))
except RequestError:
raise
except Exception:
raise RequestError(str(e)) from e
except RequestError: # pylint: disable=try-except-raise
raise # Re-raise
except Exception as e:
logger.exception('Saving group')
raise self.invalidRequestException()
raise self.invalidRequestException() from e
def deleteItem(self, parent: Authenticator, item: str) -> None:
try:
@ -492,7 +496,7 @@ class Groups(DetailHandler):
group.delete()
except Exception:
raise self.invalidItemException()
raise self.invalidItemException() from None
def servicesPools(
self, parent: Authenticator, item: str
@ -532,11 +536,10 @@ class Groups(DetailHandler):
'last_access': user.last_access,
}
res: typing.List[typing.Mapping[str, typing.Any]] = []
if group.is_meta:
# Get all users for everygroup and
groups = getGroupsFromMeta((group,))
tmpSet = None
tmpSet: typing.Optional[typing.Set] = None
for g in groups:
gSet = set((i for i in g.users.all()))
if tmpSet is None:
@ -549,12 +552,9 @@ class Groups(DetailHandler):
if not tmpSet:
break # If already empty, stop
users = list(tmpSet) if tmpSet else list()
users = list(tmpSet or {}) if tmpSet else []
tmpSet = None
else:
users = group.users.all()
for i in users:
res.append(info(i))
return res
return [info(i) for i in users]

View File

@ -45,7 +45,8 @@ from uds.core.ui import gui as uiGui
from uds.core.util import log
from uds.core.util import permissions
from uds.core.util.model import processUuid
from uds.core import Module, exceptions as g_exceptions
from uds.core.module import Module
from uds.core import exceptions as g_exceptions
from uds.models import Tag, TaggingMixin, ManagedObjectModel, Network
@ -70,7 +71,7 @@ OK: typing.Final[
str
] = 'ok' # Constant to be returned when result is just "operation complete successfully"
# pylint: disable=unused-argument
class BaseModelHandler(Handler):
"""
Base Handler for Master & Detail Handlers
@ -245,12 +246,12 @@ class BaseModelHandler(Handler):
if not permissions.hasAccess(self._user, obj, permission, root):
raise self.accessDenied()
def getPermissions(
self, obj: models.Model, root: bool = False
) -> int:
def getPermissions(self, obj: models.Model, root: bool = False) -> int:
return permissions.getEffectivePermission(self._user, obj, root)
def typeInfo(self, type_: typing.Type['Module']) -> typing.Dict[str, typing.Any]:
def typeInfo(
self, type_: typing.Type['Module'] # pylint: disable=unused-argument
) -> typing.Dict[str, typing.Any]:
"""
Returns info about the type
In fact, right now, it returns an empty dict, that will be extended by typeAsDict
@ -315,7 +316,7 @@ class BaseModelHandler(Handler):
args[key] = self._params[key]
# del self._params[key]
except KeyError as e:
raise exceptions.RequestError('needed parameter not found in data {0}'.format(e))
raise exceptions.RequestError(f'needed parameter not found in data {e}')
return args
@ -350,7 +351,7 @@ class BaseModelHandler(Handler):
:param message: Custom message to add to exception. If it is None, "Invalid Request" is used
"""
message = message or _('Invalid Request')
return exceptions.RequestError('{} {}: {}'.format(message, self.__class__, self._args))
return exceptions.RequestError(f'{message} {self.__class__}: {self._args}')
def invalidResponseException(
self, message: typing.Optional[str] = None
@ -376,10 +377,14 @@ class BaseModelHandler(Handler):
return exceptions.NotFound(message)
# raise NotFound('{} {}: {}'.format(message, self.__class__, self._args))
def accessDenied(self, message: typing.Optional[str] = None) -> exceptions.HandlerError:
def accessDenied(
self, message: typing.Optional[str] = None
) -> exceptions.HandlerError:
return exceptions.AccessDenied(message or _('Access denied'))
def notSupported(self, message: typing.Optional[str] = None) -> exceptions.HandlerError:
def notSupported(
self, message: typing.Optional[str] = None
) -> exceptions.HandlerError:
return exceptions.NotSupportedError(message or _('Operation not supported'))
# Success methods
@ -390,7 +395,7 @@ class BaseModelHandler(Handler):
logger.debug('Returning success on %s %s', self.__class__, self._args)
return OK
def test(self, type_: str):
def test(self, type_: str) -> None: # pylint: disable=unused-argument
"""
Invokes a test for an item
"""
@ -439,7 +444,7 @@ class DetailHandler(BaseModelHandler):
path: str,
params: typing.Any,
*args: str,
**kwargs: typing.Any
**kwargs: typing.Any,
): # pylint: disable=super-init-not-called
"""
Detail Handlers in fact "disabled" handler most initialization, that is no needed because
@ -472,9 +477,8 @@ class DetailHandler(BaseModelHandler):
return None
def get(
self,
) -> typing.Any: # pylint: disable=too-many-branches,too-many-return-statements
# pylint: disable=too-many-branches,too-many-return-statements
def get(self) -> typing.Any:
"""
Processes GET method for a detail Handler
"""
@ -595,7 +599,7 @@ class DetailHandler(BaseModelHandler):
# return []
# return {} # Returns one item
raise NotImplementedError(
'Must provide an getItems method for {} class'.format(self.__class__)
f'Must provide an getItems method for {self.__class__} class'
)
# Default save
@ -852,10 +856,12 @@ class ModelHandler(BaseModelHandler):
logger.debug('After filtering: %s', res)
return res
except:
except Exception as e:
logger.exception('Exception:')
logger.info('Filtering expression %s is invalid!', self.fltr)
raise exceptions.RequestError('Filtering expression {} is invalid'.format(self.fltr))
raise exceptions.RequestError(
f'Filtering expression {self.fltr} is invalid'
) from e
# Helper to process detail
# Details can be managed (writen) by any user that has MANAGEMENT permission over parent
@ -883,9 +889,8 @@ class ModelHandler(BaseModelHandler):
if not self.detail:
raise self.invalidRequestException()
detailCls = self.detail[
self._args[1]
] # pylint: disable=unsubscriptable-object
# pylint: disable=unsubscriptable-object
detailCls = self.detail[self._args[1]]
args = list(self._args[2:])
path = self._path + '/' + '/'.join(args[:2])
detail = detailCls(
@ -894,15 +899,15 @@ class ModelHandler(BaseModelHandler):
method = getattr(detail, self._operation)
return method()
except IndexError:
raise self.invalidItemException()
except (KeyError, AttributeError):
raise self.invalidMethodException()
except IndexError as e:
raise self.invalidItemException() from e
except (KeyError, AttributeError) as e:
raise self.invalidMethodException() from e
except exceptions.HandlerError:
raise
except Exception as e:
logger.error('Exception processing detail: %s', e)
raise self.invalidRequestException()
raise self.invalidRequestException() from e
def getItems(
self, *args, **kwargs
@ -950,7 +955,6 @@ class ModelHandler(BaseModelHandler):
except Exception as e: # maybe an exception is thrown to skip an item
logger.debug('Got exception processing item from model: %s', e)
# logger.exception('Exception getting item from {0}'.format(self.model))
pass
def get(self) -> typing.Any:
"""
@ -960,6 +964,7 @@ class ModelHandler(BaseModelHandler):
self.extractFilter()
return self.doFilter(self.doGet())
# pylint: disable=too-many-return-statements
def doGet(self) -> typing.Any:
logger.debug('method GET for %s, %s', self.__class__.__name__, self._args)
nArgs = len(self._args)
@ -991,8 +996,8 @@ class ModelHandler(BaseModelHandler):
operation = None
try:
operation = getattr(self, self._args[0])
except Exception:
raise self.invalidMethodException()
except Exception as e:
raise self.invalidMethodException() from e
return operation()
@ -1020,9 +1025,9 @@ class ModelHandler(BaseModelHandler):
res = self.item_as_dict(val)
self.fillIntanceFields(val, res)
return res
except Exception:
except Exception as e:
logger.exception('Got Exception looking for item')
raise self.invalidItemException()
raise self.invalidItemException() from e
# nArgs > 1
# Request type info or gui, or detail
@ -1046,8 +1051,8 @@ class ModelHandler(BaseModelHandler):
uuid=self._args[0].lower()
) # DB maybe case sensitive??, anyway, uuids are stored in lowercase
return self.getLogs(item)
except Exception:
raise self.invalidItemException()
except Exception as e:
raise self.invalidItemException() from e
# If has detail and is requesting detail
if self.detail is not None:
@ -1072,7 +1077,7 @@ class ModelHandler(BaseModelHandler):
Processes a PUT request
"""
logger.debug('method PUT for %s, %s', self.__class__.__name__, self._args)
# Append request to _params, may be needed by some classes
# I.e. to get the user IP, server name, etc..
self._params['_request'] = self._request
@ -1147,7 +1152,7 @@ class ModelHandler(BaseModelHandler):
res = self.item_as_dict(item)
self.fillIntanceFields(item, res)
except Exception as e:
except Exception:
logger.exception('Exception on put')
if deleteOnError:
item.delete()
@ -1158,16 +1163,18 @@ class ModelHandler(BaseModelHandler):
return res
except self.model.DoesNotExist:
raise exceptions.NotFound('Item not found')
raise exceptions.NotFound('Item not found') from None
except IntegrityError: # Duplicate key probably
raise exceptions.RequestError('Element already exists (duplicate key error)')
raise exceptions.RequestError(
'Element already exists (duplicate key error)'
) from None
except (exceptions.SaveException, g_exceptions.ValidationError) as e:
raise exceptions.RequestError(str(e))
raise exceptions.RequestError(str(e)) from e
except (exceptions.RequestError, exceptions.ResponseError):
raise
except Exception:
except Exception as e:
logger.exception('Exception on put')
raise exceptions.RequestError('incorrect invocation to PUT')
raise exceptions.RequestError('incorrect invocation to PUT') from e
def delete(self) -> typing.Any:
"""
@ -1189,7 +1196,7 @@ class ModelHandler(BaseModelHandler):
self.checkDelete(item)
self.deleteItem(item)
except self.model.DoesNotExist:
raise exceptions.NotFound('Element do not exists')
raise exceptions.NotFound('Element do not exists') from None
return OK

View File

@ -38,6 +38,7 @@ import logging
from django.db import connections
from django.db.backends.signals import connection_created
# from django.db.models.signals import post_migrate
from django.dispatch import receiver
@ -50,8 +51,8 @@ logger = logging.getLogger(__name__)
# Set default ssl context unverified, as MOST servers that we will connect will be with self signed certificates...
try:
_create_unverified_https_context = ssl._create_unverified_context
ssl._create_default_https_context = _create_unverified_https_context
# _create_unverified_https_context = ssl._create_unverified_context
# ssl._create_default_https_context = _create_unverified_https_context
# Capture warnnins to logg
logging.captureWarnings(True)
@ -69,17 +70,37 @@ class UDSAppConfig(AppConfig):
# with ANY command from manage.
logger.debug('Initializing app (ready) ***************')
# Now, ensures that all dynamic elements are loadad and present
# To make sure that the packages are initialized at this point
# Now, ensures that all dynamic elements are loaded and present
# To make sure that the packages are already initialized at this point
# pylint: disable=unused-import,import-outside-toplevel
from . import services
# pylint: disable=unused-import,import-outside-toplevel
from . import auths
# pylint: disable=unused-import,import-outside-toplevel
from . import mfas
# pylint: disable=unused-import,import-outside-toplevel
from . import osmanagers
# pylint: disable=unused-import,import-outside-toplevel
from . import notifiers
# pylint: disable=unused-import,import-outside-toplevel
from . import transports
# pylint: disable=unused-import,import-outside-toplevel
from . import reports
# pylint: disable=unused-import,import-outside-toplevel
from . import dispatchers
# pylint: disable=unused-import,import-outside-toplevel
from . import plugins
# pylint: disable=unused-import,import-outside-toplevel
from . import REST
# Ensure notifications table exists on local sqlite db (called "persistent" on settings.py)
@ -96,8 +117,9 @@ default_app_config = 'uds.UDSAppConfig'
# Sets up several sqlite non existing methodsm and some optimizations on sqlite
# pylint: disable=unused-argument
@receiver(connection_created)
def extend_sqlite(connection=None, **kwargs):
def extend_sqlite(connection=None, **kwargs) -> None:
if connection and connection.vendor == "sqlite":
logger.debug('Connection vendor is sqlite, extending methods')
cursor = connection.cursor()
@ -108,4 +130,3 @@ def extend_sqlite(connection=None, **kwargs):
cursor.execute('PRAGMA mmap_size=67108864')
connection.connection.create_function("MIN", 2, min)
connection.connection.create_function("MAX", 2, max)
connection.connection.create_function("CEIL", 1, math.ceil)

View File

@ -100,7 +100,7 @@ class IPAuth(auths.Authenticator):
def authenticate(
self,
username: str,
credentials: str,
credentials: str, # pylint: disable=unused-argument
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> auths.AuthenticationResult:
@ -123,7 +123,7 @@ class IPAuth(auths.Authenticator):
def internalAuthenticate(
self,
username: str,
credentials: str,
credentials: str, # pylint: disable=unused-argument
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> auths.AuthenticationResult:
@ -137,8 +137,8 @@ class IPAuth(auths.Authenticator):
return auths.FAILED_AUTH
@staticmethod
def test(env, data):
return _("All seems to be fine.")
def test(env, data): # pylint: disable=unused-argument
return [True, _("Internal structures seems ok")]
def check(self):
return _("All seems to be fine.")
@ -154,11 +154,9 @@ class IPAuth(auths.Authenticator):
return ('function setVal(element, value) {{\n' # nosec: no user input, password is always EMPTY
' document.getElementById(element).value = value;\n'
'}}\n'
'setVal("id_user", "{ip}");\n'
'setVal("id_password", "{passwd}");\n'
'document.getElementById("loginform").submit();\n').format(
ip=ip, passwd=''
)
f'setVal("id_user", "{ip}");\n'
'setVal("id_password", "");\n'
'document.getElementById("loginform").submit();\n')
return 'alert("invalid authhenticator"); window.location.reload();'

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2019 Virtual Cable S.L.
# Copyright (c) 2012-2023 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -12,7 +12,7 @@
# * 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
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
@ -41,7 +41,7 @@ import dns.reversename
from django.utils.translation import gettext_noop as _
from uds.core import auths
from uds.core.ui import gui
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from uds.core.util.state import State
from uds.core.auths.auth import authLogLogin
@ -108,12 +108,13 @@ class InternalDBAuth(auths.Authenticator):
def mfaIdentifier(self, username: str) -> str:
try:
self.dbAuthenticator().users.get(name=username, state=State.ACTIVE).mfa_data
self.dbAuthenticator().users.get(name=username.lower(), state=State.ACTIVE).mfa_data
except Exception: # nosec: This is e controled pickle loading
pass
return ''
def transformUsername(self, username: str, request: 'ExtendedHttpRequest') -> str:
username = username.lower()
if self.differentForEachHost.isTrue():
newUsername = (
(request.ip_proxy if self.acceptProxy.isTrue() else request.ip)
@ -147,6 +148,7 @@ class InternalDBAuth(auths.Authenticator):
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> auths.AuthenticationResult:
username = username.lower()
logger.debug('Username: %s, Password: %s', username, credentials)
dbAuth = self.dbAuthenticator()
try:
@ -159,7 +161,7 @@ class InternalDBAuth(auths.Authenticator):
return auths.FAILED_AUTH
# Internal Db Auth has its own groups. (That is, no external source). If a group is active it is valid
if cryptoManager().checkHash(credentials, user.password):
if CryptoManager().checkHash(credentials, user.password):
groupsManager.validate([g.name for g in user.groups.all()])
return auths.SUCCESS_AUTH
@ -169,7 +171,7 @@ class InternalDBAuth(auths.Authenticator):
def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'):
dbAuth = self.dbAuthenticator()
try:
user: 'models.User' = dbAuth.users.get(name=username, state=State.ACTIVE)
user: 'models.User' = dbAuth.users.get(name=username.lower(), state=State.ACTIVE)
except Exception:
return
@ -178,7 +180,7 @@ class InternalDBAuth(auths.Authenticator):
def getRealName(self, username: str) -> str:
# Return the real name of the user, if it is set
try:
user = self.dbAuthenticator().users.get(name=username, state=State.ACTIVE)
user = self.dbAuthenticator().users.get(name=username.lower(), state=State.ACTIVE)
return user.real_name or username
except Exception:
return super().getRealName(username)
@ -187,7 +189,7 @@ class InternalDBAuth(auths.Authenticator):
pass
@staticmethod
def test(env, data):
def test(env, data): # pylint: disable=unused-argument
return [True, _("Internal structures seems ok")]
def check(self):

View File

@ -32,6 +32,6 @@
Sample authenticator. We import here the module, and uds.auths module will
take care of registering it as provider
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from .authenticator import RadiusAuth

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
@ -37,7 +37,7 @@ from django.utils.translation import gettext_noop as _
from uds.core.ui import gui
from uds.core import auths
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from uds.core.auths.auth import authLogLogin
from . import client
@ -207,9 +207,9 @@ class RadiusAuth(auths.Authenticator):
connection = self.radiusClient()
# Reply is not important...
connection.authenticate(
cryptoManager().randomString(10), cryptoManager().randomString(10)
CryptoManager().randomString(10), CryptoManager().randomString(10)
)
except client.RadiusAuthenticationError as e:
except client.RadiusAuthenticationError:
pass
except Exception:
logger.exception('Connecting')

View File

@ -207,9 +207,6 @@ class RadiusClient:
if reply.code == pyrad.packet.AccessChallenge:
state = typing.cast(typing.List[bytes], reply.get('State') or [b''])[0]
replyMessage = typing.cast(
typing.List[bytes], reply.get('Reply-Message') or ['']
)[0]
return self.challenge_only(username, otp, state=state)
# user/pwd accepted: but this user does not have challenge data
@ -229,7 +226,7 @@ class RadiusClient:
return RadiusResult()
def authenticate_challenge(
self, username: str, password: str = '', otp: str = '', state: bytes = b''
self, username: str, password: str = '', otp: str = '', state: bytes = b'' # nosec: not a password, just an empty string
) -> RadiusResult:
'''
wrapper for above 3 functions: authenticate_only, challenge_only, authenticate_and_challenge

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# pylint: disable=no-member
#
# Copyright (c) 2012-2022 Virtual Cable S.L.U.
@ -269,10 +269,10 @@ class RegexLdap(auths.Authenticator):
pattern = '(' + pattern + ')'
try:
re.search(pattern, '')
except Exception:
except Exception as e:
raise exceptions.ValidationError(
'Invalid pattern in {0}: {1}'.format(fieldLabel, line)
)
f'Invalid pattern in {fieldLabel}: {line}'
) from e
def __getAttrsFromField(self, field: str) -> typing.List[str]:
res = []
@ -486,9 +486,7 @@ class RegexLdap(auths.Authenticator):
for usr in ldaputil.getAsDict(
con=self.__connection(),
base=self._ldapBase,
ldapFilter='(&(objectClass={})({}={}))'.format(
self._altClass, self._userIdAttr, ldaputil.escape(username)
),
ldapFilter=f'(&(objectClass={self._altClass})({self._userIdAttr}={ldaputil.escape(username)}))',
attrList=attributes,
sizeLimit=LDAP_RESULT_LIMIT,
):
@ -555,7 +553,7 @@ class RegexLdap(auths.Authenticator):
self.__connectAs(
usr['dn'], credentials
) # Will raise an exception if it can't connect
except:
except Exception:
authLogLogin(
request, self.dbAuthenticator(), username, 'Invalid password'
)
@ -627,9 +625,7 @@ class RegexLdap(auths.Authenticator):
for r in ldaputil.getAsDict(
con=self.__connection(),
base=self._ldapBase,
ldapFilter='(&(&(objectClass={})({}={}*)))'.format(
self._userClass, self._userIdAttr, ldaputil.escape(pattern)
),
ldapFilter=f'(&(&(objectClass={self._userClass})({self._userIdAttr}={ldaputil.escape(pattern)}*)))',
attrList=None, # All attrs
sizeLimit=LDAP_RESULT_LIMIT,
):
@ -642,11 +638,11 @@ class RegexLdap(auths.Authenticator):
)
logger.debug(res)
return res
except Exception:
except Exception as e:
logger.exception("Exception: ")
raise auths.exceptions.AuthenticatorException(
_('Too many results, be more specific')
)
) from e
@staticmethod
def test(env, data):
@ -676,7 +672,7 @@ class RegexLdap(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # ldap.SCOPE_* not resolved due to dynamic creation?
filterstr='(objectClass=%s)' % self._userClass,
filterstr=f'(objectClass={self._userClass})',
sizelimit=1,
)
)
@ -700,8 +696,7 @@ class RegexLdap(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # ldap.SCOPE_* not resolved due to dynamic creation?
filterstr='(&(objectClass=%s)(%s=*))'
% (self._userClass, self._userIdAttr),
filterstr=f'(&(objectClass={self._userClass})({self._userIdAttr}=*))',
sizelimit=1,
)
)
@ -728,7 +723,7 @@ class RegexLdap(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # ldap.SCOPE_* not resolved due to dynamic creation?
filterstr='(%s=*)' % vals,
filterstr=f'({vals}=*)',
sizelimit=1,
)
)
@ -759,15 +754,8 @@ class RegexLdap(auths.Authenticator):
]
def __str__(self):
return "Ldap Auth: {}:{}@{}:{}, base = {}, userClass = {}, userIdAttr = {}, groupNameAttr = {}, userName attr = {}, altClass={}".format(
self._username,
self._password,
self._host,
self._port,
self._ldapBase,
self._userClass,
self._userIdAttr,
self._groupNameAttr,
self._userNameAttr,
self._altClass,
return (
f'Ldap Auth: {self._username}:{self._password}@{self._host}:{self._port},'
f' base = {self._ldapBase}, userClass = {self._userClass}, userIdAttr = {self._userIdAttr},'
f' groupNameAttr = {self._groupNameAttr}, userName attr = {self._userNameAttr}, altClass={self._altClass}'
)

View File

@ -27,7 +27,7 @@
# 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.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from uds.core import managers
from .saml import SAMLAuthenticator # import for registration on space,

View File

@ -4,9 +4,7 @@ from uds.core.util.config import Config
ORGANIZATION_NAME = Config.section('SAML').value('Organization Name', 'UDS', help='Organization name to display on SAML SP Metadata')
ORGANIZATION_DISPLAY = Config.section('SAML').value('Org. Display Name', 'UDS Organization', help='Organization Display name to display on SAML SP Metadata')
ORGANIZATION_URL = Config.section('SAML').value('Organization URL', 'http://www.udsenterprise.com', help='Organization url to display on SAML SP Metadata')
IDP_METADATA_CACHE = Config.section('SAML').value('IDP Metadata cache')
ORGANIZATION_NAME.get()
ORGANIZATION_DISPLAY.get()
ORGANIZATION_URL.get()
IDP_METADATA_CACHE.getInt()

View File

@ -28,29 +28,29 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import re
from urllib.parse import urlparse
import xml.sax # nosec: used to parse trusted xml provided only by administrators
import datetime
import requests
import logging
import typing
import requests
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from django.utils.translation import gettext_noop as _, gettext
from uds.models import getSqlDatetime
from uds.core.util.model import getSqlDatetime
from uds.core.ui import gui
from uds.core import auths, exceptions
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from uds.core.util.decorators import allowCache
from uds.core.util import certs
from uds.core.util import security
from . import config
@ -121,9 +121,7 @@ class SAMLAuthenticator(auths.Authenticator):
multiline=10,
label=_('Private key'),
order=1,
tooltip=_(
'Private key used for sign and encription, as generated in base 64 from openssl'
),
tooltip=_('Private key used for sign and encription, as generated in base 64 from openssl'),
required=True,
tab=_('Certificates'),
)
@ -143,9 +141,7 @@ class SAMLAuthenticator(auths.Authenticator):
multiline=4,
label=_('IDP Metadata'),
order=3,
tooltip=_(
'You can enter here the URL or the IDP metadata or the metadata itself (xml)'
),
tooltip=_('You can enter here the URL or the IDP metadata or the metadata itself (xml)'),
required=True,
tab=_('Metadata'),
)
@ -153,9 +149,7 @@ class SAMLAuthenticator(auths.Authenticator):
length=256,
label=_('Entity ID'),
order=4,
tooltip=_(
'ID of the SP. If left blank, this will be autogenerated from server URL'
),
tooltip=_('ID of the SP. If left blank, this will be autogenerated from server URL'),
tab=_('Metadata'),
)
@ -334,19 +328,14 @@ class SAMLAuthenticator(auths.Authenticator):
if ' ' in values['name']:
raise exceptions.ValidationError(
gettext(
'This kind of Authenticator does not support white spaces on field NAME'
)
gettext('This kind of Authenticator does not support white spaces on field NAME')
)
# First, validate certificates
self.cache.remove('idpMetadata')
# This is in fact not needed, but we may say something useful to user if we check this
if (
self.serverCertificate.value.startswith('-----BEGIN CERTIFICATE-----\n')
is False
):
if self.serverCertificate.value.startswith('-----BEGIN CERTIFICATE-----\n') is False:
raise exceptions.ValidationError(
gettext(
'Server certificate should be a valid PEM (PEM certificates starts with -----BEGIN CERTIFICATE-----)'
@ -354,17 +343,13 @@ class SAMLAuthenticator(auths.Authenticator):
)
try:
cryptoManager().loadCertificate(self.serverCertificate.value)
CryptoManager().loadCertificate(self.serverCertificate.value)
except Exception as e:
raise exceptions.ValidationError(
gettext('Invalid server certificate. ') + str(e)
)
raise exceptions.ValidationError(gettext('Invalid server certificate. ') + str(e))
if (
self.privateKey.value.startswith('-----BEGIN RSA PRIVATE KEY-----\n')
is False
and self.privateKey.value.startswith('-----BEGIN PRIVATE KEY-----\n')
is False
self.privateKey.value.startswith('-----BEGIN RSA PRIVATE KEY-----\n') is False
and self.privateKey.value.startswith('-----BEGIN PRIVATE KEY-----\n') is False
):
raise exceptions.ValidationError(
gettext(
@ -373,14 +358,14 @@ class SAMLAuthenticator(auths.Authenticator):
)
try:
pk = cryptoManager().loadPrivateKey(self.privateKey.value)
CryptoManager().loadPrivateKey(self.privateKey.value)
except Exception as e:
raise exceptions.ValidationError(gettext('Invalid private key. ') + str(e))
if not certs.checkCertificateMatchPrivateKey(cert=self.serverCertificate.value, key=self.privateKey.value):
raise exceptions.ValidationError(
gettext('Certificate and private key do not match')
)
if not security.checkCertificateMatchPrivateKey(
cert=self.serverCertificate.value, key=self.privateKey.value
):
raise exceptions.ValidationError(gettext('Certificate and private key do not match'))
request: 'ExtendedHttpRequest' = values['_request']
@ -395,14 +380,14 @@ class SAMLAuthenticator(auths.Authenticator):
logger.debug('idp Metadata is an URL: %s', idpMetadata)
try:
resp = requests.get(
idpMetadata.split('\n')[0], verify=self.checkSSLCertificate.isTrue()
idpMetadata.split('\n')[0],
verify=self.checkSSLCertificate.isTrue(),
timeout=10,
)
idpMetadata = resp.content.decode()
except Exception as e:
raise exceptions.ValidationError(
gettext('Can\'t fetch url {0}: {1}').format(
self.idpMetadata.value, str(e)
)
gettext('Can\'t fetch url {0}: {1}').format(self.idpMetadata.value, str(e))
)
fromUrl = True
@ -412,9 +397,7 @@ class SAMLAuthenticator(auths.Authenticator):
xml.sax.parseString(idpMetadata, xml.sax.ContentHandler()) # type: ignore # nosec: url provided by admin
except Exception as e:
msg = (gettext(' (obtained from URL)') if fromUrl else '') + str(e)
raise exceptions.ValidationError(
gettext('XML does not seem valid for IDP Metadata ') + msg
)
raise exceptions.ValidationError(gettext('XML does not seem valid for IDP Metadata ') + msg)
# Now validate regular expressions, if they exists
self.validateField(self.userNameAttr)
@ -424,12 +407,17 @@ class SAMLAuthenticator(auths.Authenticator):
def getReqFromRequest(
self,
request: 'ExtendedHttpRequest',
params: typing.Dict[str, typing.Any] = {},
params: typing.Optional[typing.Dict[str, typing.Any]] = None,
) -> typing.Dict[str, typing.Any]:
manageUrlObj = urlparse(self.manageUrl.value)
script_path = manageUrlObj.path
# If callback parameters are passed, we use them
if params:
# Remove next 3 lines, just for testing and debugging
# params['http_host'] = '172.27.0.1'
# params['server_port'] = '8000'
# params['https'] = False
return {
'https': ['off', 'on'][params.get('https', False)],
'http_host': params['http_host'],
@ -455,26 +443,20 @@ class SAMLAuthenticator(auths.Authenticator):
@allowCache(
cachePrefix='idpm',
cachingKeyFnc=CACHING_KEY_FNC,
cacheTimeout=3600*24*365, # 1 year
cacheTimeout=3600 * 24 * 365, # 1 year
)
def getIdpMetadataDict(self, **kwargs) -> typing.Dict[str, typing.Any]:
def getIdpMetadataDict(self) -> typing.Dict[str, typing.Any]:
if self.idpMetadata.value.startswith('http'):
try:
resp = requests.get(
self.idpMetadata.value.split('\n')[0],
verify=self.checkSSLCertificate.isTrue(),
timeout=10,
)
val = resp.content.decode()
except Exception as e:
logger.error('Error fetching idp metadata: %s', e)
raise auths.exceptions.AuthenticatorException(
gettext('Can\'t access idp metadata')
)
self.cache.put(
'idpMetadata',
val,
config.IDP_METADATA_CACHE.getInt(True),
)
raise auths.exceptions.AuthenticatorException(gettext('Can\'t access idp metadata'))
else:
val = self.idpMetadata.value
@ -530,6 +512,11 @@ class SAMLAuthenticator(auths.Authenticator):
},
}
@allowCache(
cachePrefix='spm',
cachingKeyFnc=CACHING_KEY_FNC,
cacheTimeout=3600, # 1 hour
)
def getSpMetadata(self) -> str:
saml_settings = OneLogin_Saml2_Settings(settings=self.oneLoginSettings())
metadata = saml_settings.get_sp_metadata()
@ -540,8 +527,7 @@ class SAMLAuthenticator(auths.Authenticator):
)
if isinstance(metadata, str):
return metadata
else:
return typing.cast(bytes, metadata).decode()
return typing.cast(bytes, metadata).decode()
def validateField(self, field: gui.TextField):
"""
@ -555,14 +541,10 @@ class SAMLAuthenticator(auths.Authenticator):
pattern = '(' + pattern + ')'
try:
re.search(pattern, '')
except:
raise exceptions.ValidationError(
'Invalid pattern at {0}: {1}'.format(field.label, line)
)
except Exception as e:
raise exceptions.ValidationError(f'Invalid pattern at {field.label}: {line}') from e
def processField(
self, field: str, attributes: typing.Dict[str, typing.List]
) -> typing.List[str]:
def processField(self, field: str, attributes: typing.Dict[str, typing.List]) -> typing.List[str]:
res = []
for line in field.splitlines():
equalPos = line.find('=')
@ -600,9 +582,7 @@ class SAMLAuthenticator(auths.Authenticator):
content_type = 'text/html' if wantsHtml else 'application/samlmetadata+xml'
info = (
'<br/>'.join(info.replace('<', '&lt;').splitlines())
if parameters.get('format') == 'html'
else info
'<br/>'.join(info.replace('<', '&lt;').splitlines()) if parameters.get('format') == 'html' else info
)
return info, content_type # 'application/samlmetadata+xml')
@ -627,19 +607,27 @@ class SAMLAuthenticator(auths.Authenticator):
else:
req['get_data']['SAMLResponse'] = req['post_data']['SAMLResponse']
logoutRequestId = request.session.get('samlLogoutRequestId', None)
# Cleanup session & session cookie
request.session.flush()
settings = OneLogin_Saml2_Settings(settings=self.oneLoginSettings())
auth = OneLogin_Saml2_Auth(req, settings)
dscb = lambda: request.session.flush()
url = auth.process_slo(delete_session_cb=dscb)
url = auth.process_slo(request_id=logoutRequestId)
errors = auth.get_errors()
if errors:
raise auths.exceptions.AuthenticatorException(
gettext('Error processing SLO: ') + str(errors)
)
logger.debug('Error on SLO: %s', auth.get_last_response_xml())
logger.debug('post_data: %s', req['post_data'])
logger.info('Errors processing logout request: %s', errors)
raise auths.exceptions.AuthenticatorException(gettext('Error processing SLO: ') + str(errors))
# Remove MFA related data
if request.user:
self.mfaClean(request.user.name)
return auths.AuthenticationResult(
success=auths.AuthenticationSuccess.REDIRECT,
@ -663,19 +651,13 @@ class SAMLAuthenticator(auths.Authenticator):
auth = OneLogin_Saml2_Auth(req, settings)
auth.process_response()
except Exception as e:
raise auths.exceptions.AuthenticatorException(
gettext('Error processing SAML response: ') + str(e)
)
raise auths.exceptions.AuthenticatorException(gettext('Error processing SAML response: ') + str(e))
errors = auth.get_errors()
if errors:
raise auths.exceptions.AuthenticatorException(
'SAML response error: ' + str(errors)
)
raise auths.exceptions.AuthenticatorException('SAML response error: ' + str(errors))
if not auth.is_authenticated():
raise auths.exceptions.AuthenticatorException(
gettext('SAML response not authenticated')
)
raise auths.exceptions.AuthenticatorException(gettext('SAML response not authenticated'))
# Store SAML attributes
request.session['SAML'] = {
@ -703,13 +685,13 @@ class SAMLAuthenticator(auths.Authenticator):
attributes.update(auth.get_friendlyname_attributes())
if not attributes:
raise auths.exceptions.AuthenticatorException(
gettext('No attributes returned from IdP')
)
raise auths.exceptions.AuthenticatorException(gettext('No attributes returned from IdP'))
logger.debug("Attributes: %s", attributes)
# Now that we have attributes, we can extract values from this, map groups, etc...
username = ''.join(self.processField(self.userNameAttr.value, attributes))
username = ''.join(
self.processField(self.userNameAttr.value, attributes)
) # in case of multiple values is returned, join them
logger.debug('Username: %s', username)
groups = self.processField(self.groupNameAttr.value, attributes)
@ -721,16 +703,22 @@ class SAMLAuthenticator(auths.Authenticator):
# store groups for this username at storage, so we can check it at a later stage
self.storage.putPickle(username, [realName, groups])
# store also the mfa identifier field value, in case we have provided it
if self.mfaAttr.value.strip():
self.storage.putPickle(
self.mfaStorageKey(username),
''.join(self.processField(self.mfaAttr.value, attributes)),
) # in case multipel values is returned, join them
else:
self.storage.remove(self.mfaStorageKey(username))
# Now we check validity of user
gm.validate(groups)
return auths.AuthenticationResult(
success=auths.AuthenticationSuccess.OK, username=username
)
return auths.AuthenticationResult(success=auths.AuthenticationSuccess.OK, username=username)
def logout(
self, request: 'ExtendedHttpRequest', username: str
) -> auths.AuthenticationResult:
def logout(self, request: 'ExtendedHttpRequest', username: str) -> auths.AuthenticationResult:
if not self.globalLogout.isTrue():
return auths.SUCCESS_AUTH
@ -742,6 +730,15 @@ class SAMLAuthenticator(auths.Authenticator):
saml = request.session.get('SAML', {})
# Clear user data from session
request.session.clear()
# Remove MFA related data
self.mfaClean(username)
if not saml:
return auths.SUCCESS_AUTH
return auths.AuthenticationResult(
success=auths.AuthenticationSuccess.REDIRECT,
url=auth.logout(
@ -772,7 +769,7 @@ class SAMLAuthenticator(auths.Authenticator):
req = self.getReqFromRequest(request)
auth = OneLogin_Saml2_Auth(req, self.oneLoginSettings())
return 'window.location="{0}";'.format(auth.login())
return f'window.location="{auth.login()}";'
def removeUser(self, username):
"""

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
@ -118,7 +118,9 @@ class SampleAuth(auths.Authenticator):
# : We will define a simple form where we will use a simple
# : list editor to allow entering a few group names
groups = gui.EditableListField(label=_('Groups'), values=['Gods', 'Daemons', 'Mortals'])
groups = gui.EditableListField(
label=_('Groups'), values=['Gods', 'Daemons', 'Mortals']
)
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
"""
@ -131,9 +133,7 @@ class SampleAuth(auths.Authenticator):
# unserialization, and at this point all will be default values
# so self.groups.value will be []
if values and len(self.groups.value) < 2:
raise exceptions.ValidationError(
_('We need more than two groups!')
)
raise exceptions.ValidationError(_('We need more than two groups!'))
def searchUsers(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]:
"""
@ -147,8 +147,8 @@ class SampleAuth(auths.Authenticator):
"""
return [
{
'id': '{0}-{1}'.format(pattern, a),
'name': '{0} number {1}'.format(pattern, a),
'id': f'{pattern}-{a}',
'name': f'{pattern} number {a}',
}
for a in range(1, 10)
]
@ -173,7 +173,7 @@ class SampleAuth(auths.Authenticator):
username: str,
credentials: str,
groupsManager: 'GroupsManager',
request: 'ExtendedHttpRequest',
request: 'ExtendedHttpRequest', # pylint: disable=unused-argument
) -> auths.AuthenticationResult:
"""
This method is invoked by UDS whenever it needs an user to be authenticated.
@ -246,7 +246,9 @@ class SampleAuth(auths.Authenticator):
if len(set(g.lower()).intersection(username.lower())) >= 2:
groupsManager.validate(g)
def getJavascript(self, request: 'HttpRequest') -> typing.Optional[str]:
def getJavascript(
self, request: 'HttpRequest' # pylint: disable=unused-argument
) -> typing.Optional[str]:
"""
If we override this method from the base one, we are telling UDS
that we want to draw our own authenticator.
@ -278,7 +280,10 @@ class SampleAuth(auths.Authenticator):
return res
def authCallback(
self, parameters: typing.Dict[str, typing.Any], gm: 'auths.GroupsManager', request: 'ExtendedHttpRequestWithUser'
self,
parameters: typing.Dict[str, typing.Any],
gm: 'auths.GroupsManager', # pylint: disable=unused-argument
request: 'ExtendedHttpRequestWithUser', # pylint: disable=unused-argument
) -> AuthenticationResult:
"""
We provide this as a sample of callback for an user.
@ -313,7 +318,7 @@ class SampleAuth(auths.Authenticator):
Here, we will set the state to "Inactive" and realName to the same as username, but twice :-)
"""
from uds.core.util.state import State
from uds.core.util.state import State # pylint: disable=import-outside-toplevel
usrData['real_name'] = usrData['name'] + ' ' + usrData['name']
usrData['state'] = State.INACTIVE

View File

@ -32,7 +32,7 @@
Sample authenticator. We import here the module, and uds.auths module will
take care of registering it as provider
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from .SampleAuth import SampleAuth

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# pylint: disable=no-member # ldap module gives errors to pylint
#
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
@ -51,9 +50,8 @@ logger = logging.getLogger(__name__)
LDAP_RESULT_LIMIT = 100
# pylint: disable=too-many-instance-attributes
class SimpleLDAPAuthenticator(auths.Authenticator):
host = gui.TextField(
length=64,
label=_('Host'),
@ -192,13 +190,12 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
order=13,
tooltip=_('Attribute from where to extract the MFA code'),
required=False,
tab=gui.MFA_TAB,
tab=gui.Tab.MFA,
)
typeName = _('SimpleLDAP (DEPRECATED)')
typeName = _('SimpleLDAP')
typeType = 'SimpleLdapAuthenticator'
typeDescription = _('Simple LDAP authenticator (DEPRECATED)')
typeDescription = _('Simple LDAP authenticator')
iconFile = 'auth.png'
# If it has and external source where to get "new" users (groups must be declared inside UDS)
@ -230,7 +227,6 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
_verifySsl: bool = True
_certificate: str = ''
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
if values:
self._host = values['host']
@ -319,12 +315,8 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
) = vals[1:14]
self._ssl = gui.toBool(ssl)
if vals[0] == 'v2':
(
self._mfaAttr,
verifySsl,
self._certificate
) = vals[14:17]
if vals[0] == 'v2':
(self._mfaAttr, verifySsl, self._certificate) = vals[14:17]
self._verifySsl = gui.toBool(verifySsl)
def mfaStorageKey(self, username: str) -> str:
@ -333,9 +325,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
def mfaIdentifier(self, username: str) -> str:
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
def __connection(
self
):
def __connection(self):
"""
Tries to connect to ldap. If username is None, it tries to connect using user provided credentials.
@return: Connection established
@ -376,7 +366,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
@return: None if username is not found, an dictionary of LDAP entry attributes if found.
@note: Active directory users contains the groups it belongs to in "memberOf" attribute
"""
attributes = [i for i in self._userNameAttr.split(',') + [self._userIdAttr]]
attributes = self._userNameAttr.split(',') + [self._userIdAttr]
if self._mfaAttr:
attributes = attributes + [self._mfaAttr]
@ -410,13 +400,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
try:
groups: typing.List[str] = []
filter_ = '(&(objectClass=%s)(|(%s=%s)(%s=%s)))' % (
self._groupClass,
self._memberAttr,
user['_id'],
self._memberAttr,
user['dn'],
)
filter_ = f'(&(objectClass={self._groupClass})(|({self._memberAttr}={user["_id"]})({self._memberAttr}={user["dn"]})))'
for d in ldaputil.getAsDict(
con=self.__connection(),
base=self._ldapBase,
@ -450,7 +434,11 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
).strip()
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager', request: 'ExtendedHttpRequest'
self,
username: str,
credentials: str,
groupsManager: 'auths.GroupsManager',
request: 'ExtendedHttpRequest',
) -> auths.AuthenticationResult:
'''
Must authenticate the user.
@ -466,9 +454,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
user = self.__getUser(username)
if user is None:
authLogLogin(
request, self.dbAuthenticator(), username, 'Invalid user'
)
authLogLogin(request, self.dbAuthenticator(), username, 'Invalid user')
return auths.FAILED_AUTH
try:
@ -476,7 +462,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
self.__connectAs(
user['dn'], credentials
) # Will raise an exception if it can't connect
except:
except Exception:
authLogLogin(
request, self.dbAuthenticator(), username, 'Invalid password'
)
@ -556,8 +542,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
for r in ldaputil.getAsDict(
con=self.__connection(),
base=self._ldapBase,
ldapFilter='(&(objectClass=%s)(%s=%s*))'
% (self._userClass, self._userIdAttr, pattern),
ldapFilter=f'(&(objectClass={self._userClass})({self._userIdAttr}={pattern}*))',
attrList=[self._userIdAttr, self._userNameAttr],
sizeLimit=LDAP_RESULT_LIMIT,
):
@ -569,11 +554,11 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
)
return res
except Exception:
except Exception as e:
logger.exception("Exception: ")
raise auths.exceptions.AuthenticatorException(
_('Too many results, be more specific')
)
) from e
def searchGroups(self, pattern: str) -> typing.Iterable[typing.Dict[str, str]]:
try:
@ -581,19 +566,18 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
for r in ldaputil.getAsDict(
con=self.__connection(),
base=self._ldapBase,
ldapFilter='(&(objectClass=%s)(%s=%s*))'
% (self._groupClass, self._groupIdAttr, pattern),
ldapFilter=f'(&(objectClass={self._groupClass})({self._groupIdAttr}={pattern}*))',
attrList=[self._groupIdAttr, 'memberOf', 'description'],
sizeLimit=LDAP_RESULT_LIMIT,
):
res.append({'id': r[self._groupIdAttr][0], 'name': r['description'][0]})
return res
except Exception:
except Exception as e:
logger.exception("Exception: ")
raise auths.exceptions.AuthenticatorException(
_('Too many results, be more specific')
)
) from e
@staticmethod
def test(env, data) -> typing.List[typing.Any]:
@ -620,7 +604,17 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
return [False, _('Ldap search base is incorrect')]
try:
if len(con.search_ext_s(base=self._ldapBase, scope=ldap.SCOPE_SUBTREE, filterstr='(objectClass=%s)' % self._userClass, sizelimit=1)) == 1: # type: ignore # SCOPE.. exists on LDAP after load
if (
len(
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # SCOPE.. exists on LDAP after load
filterstr=f'(objectClass={self._userClass})',
sizelimit=1,
)
)
== 1
):
raise Exception()
return [
False,
@ -628,7 +622,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
'Ldap user class seems to be incorrect (no user found by that class)'
),
]
except Exception as e: # nosec: Flow control
except Exception: # nosec: Flow control
# If found 1 or more, all right
pass
@ -638,7 +632,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # SCOPE.. exists on LDAP after load
filterstr='(objectClass=%s)' % self._groupClass,
filterstr=f'(objectClass={self._groupClass})',
sizelimit=1,
)
)
@ -651,7 +645,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
'Ldap group class seems to be incorrect (no group found by that class)'
),
]
except Exception as e: # nosec: Flow control
except Exception: # nosec: Flow control
# If found 1 or more, all right
pass
@ -661,7 +655,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # SCOPE.. exists on LDAP after load
filterstr='(%s=*)' % self._userIdAttr,
filterstr=f'({self._userIdAttr}=*)',
sizelimit=1,
)
)
@ -674,7 +668,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
'Ldap user id attribute seems to be incorrect (no user found by that attribute)'
),
]
except Exception as e: # nosec: Flow control
except Exception: # nosec: Flow control
# If found 1 or more, all right
pass
@ -684,7 +678,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # SCOPE.. exists on LDAP after load
filterstr='(%s=*)' % self._groupIdAttr,
filterstr=f'({self._groupIdAttr}=*)',
sizelimit=1,
)
)
@ -697,7 +691,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
'Ldap group id attribute seems to be incorrect (no group found by that attribute)'
),
]
except Exception as e: # nosec: Flow control
except Exception: # nosec: Flow control
# If found 1 or more, all right
pass
@ -708,8 +702,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # SCOPE.. exists on LDAP after load
filterstr='(&(objectClass=%s)(%s=*))'
% (self._userClass, self._userIdAttr),
filterstr=f'(&(objectClass={self._userClass})({self._userIdAttr}=*))',
sizelimit=1,
)
)
@ -722,7 +715,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
'Ldap user class or user id attr is probably wrong (can\'t find any user with both conditions)'
),
]
except Exception as e: # nosec: Flow control
except Exception: # nosec: Flow control
# If found 1 or more, all right
pass
@ -731,8 +724,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
res = con.search_ext_s(
base=self._ldapBase,
scope=ldap.SCOPE_SUBTREE, # type: ignore # SCOPE.. exists on LDAP after load
filterstr='(&(objectClass=%s)(%s=*))'
% (self._groupClass, self._groupIdAttr),
filterstr=f'(&(objectClass={self._groupClass})({self._groupIdAttr}=*))',
attrlist=[self._memberAttr],
)
if not res:
@ -759,16 +751,9 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
]
def __str__(self):
return "Ldap Auth: {0}:{1}@{2}:{3}, base = {4}, userClass = {5}, groupClass = {6}, userIdAttr = {7}, groupIdAttr = {8}, memberAttr = {9}, userName attr = {10}".format(
self._username,
self._password,
self._host,
self._port,
self._ldapBase,
self._userClass,
self._groupClass,
self._userIdAttr,
self._groupIdAttr,
self._memberAttr,
self._userNameAttr,
return (
f'Ldap Auth: {self._username}:{self._password}@{self._host}:{self._port}, '
f'base = {self._ldapBase}, userClass = {self._userClass}, groupClass = {self._groupClass}, '
f'userIdAttr = {self._userIdAttr}, groupIdAttr = {self._groupIdAttr}, '
f'memberAttr = {self._memberAttr}, userName attr = {self._userNameAttr}'
)

View File

@ -38,7 +38,7 @@ To create a new authentication module, you will need to follow this steps:
The registration of modules is done locating subclases of :py:class:`uds.core.auths.Authentication`
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from uds.core.util import modfinder

View File

@ -34,13 +34,8 @@ This package contains all core-related code for UDS
"""
import time
# Core needs tasks manager to register scheduled jobs, so we ensure of that here
from .environment import Environmentable
from .serializable import Serializable
from .module import Module
VERSION = '4.x.x-DEVEL'
VERSION_STAMP = '{}-DEVEL'.format(time.strftime("%Y%m%d"))
VERSION_STAMP = f'{time.strftime("%Y%m%d")}-DEVEL'
# Minimal uds client version required to connect to this server
REQUIRED_CLIENT_VERSION = '3.5.0'
REQUIRED_CLIENT_VERSION = '3.6.0'

View File

@ -30,7 +30,7 @@
"""
UDS authentication related interfaces and classes
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from .authenticator import (
Authenticator,

View File

@ -30,7 +30,7 @@
Provides useful functions for authenticating, used by web interface.
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import logging
import typing
@ -54,7 +54,7 @@ from uds.core.util import net
from uds.core.util.config import GlobalConfig
from uds.core.util.stats import events
from uds.core.util.state import State
from uds.core.managers import cryptoManager
from uds.core.managers.crypto import CryptoManager
from uds.core.auths import Authenticator as AuthenticatorInstance, SUCCESS_AUTH
from uds import models
@ -73,6 +73,7 @@ EXPIRY_KEY = 'ek'
AUTHORIZED_KEY = 'ak'
ROOT_ID = -20091204 # Any negative number will do the trick
UDS_COOKIE_LENGTH = 48
IP_KEY = 'session_ip'
RT = typing.TypeVar('RT')
@ -91,9 +92,14 @@ def getUDSCookie(
Generates a random cookie for uds, used, for example, to encript things
"""
if 'uds' not in request.COOKIES:
cookie = cryptoManager().randomString(UDS_COOKIE_LENGTH)
cookie = CryptoManager().randomString(UDS_COOKIE_LENGTH)
if response is not None:
response.set_cookie('uds', cookie, samesite='Lax')
response.set_cookie(
'uds',
cookie,
samesite='Lax',
httponly=GlobalConfig.ENHANCED_SECURITY.getBool(),
)
request.COOKIES['uds'] = cookie
else:
cookie = request.COOKIES['uds'][:UDS_COOKIE_LENGTH]
@ -163,7 +169,7 @@ def webLoginRequired(
if not request.user or not request.authorized:
return HttpResponseRedirect(reverse('page.login'))
if admin in (True, 'admin'):
if admin in (True, 'admin'):
if request.user.isStaff() is False or (
admin == 'admin' and not request.user.is_admin
):
@ -197,7 +203,7 @@ def trustedSourceRequired(
try:
if not isTrustedSource(request.ip):
return HttpResponseForbidden()
except Exception as e:
except Exception:
logger.warning(
'Error checking trusted source: "%s" does not seems to be a valid network string. Using Unrestricted access.',
GlobalConfig.TRUSTED_SOURCES.get(),
@ -397,7 +403,9 @@ def webLogin(
Helper function to, once the user is authenticated, store the information at the user session.
@return: Always returns True
"""
from uds import REST
from uds import ( # pylint: disable=import-outside-toplevel # to avoid circular imports
REST,
)
if (
user.id != ROOT_ID
@ -413,12 +421,16 @@ def webLogin(
request.authorized = (
False # For now, we don't know if the user is authorized until MFA is checked
)
# Store request ip in session
request.session[IP_KEY] = request.ip
# If Enabled zero trust, do not cache credentials
if GlobalConfig.ENFORCE_ZERO_TRUST.getBool(False):
password = '' # nosec: clear password if zero trust is enabled
request.session[USER_KEY] = user.id
request.session[PASS_KEY] = codecs.encode(cryptoManager().symCrypt(password, cookie), "base64").decode() # as str
request.session[PASS_KEY] = codecs.encode(
CryptoManager().symCrypt(password, cookie), "base64"
).decode() # as str
# Ensures that this user will have access through REST api if logged in through web interface
# Note that REST api will set the session expiry to selected value if user is an administrator
@ -444,11 +456,13 @@ def webPassword(request: HttpRequest) -> str:
"""
if hasattr(request, 'session'):
passkey = codecs.decode(request.session.get(PASS_KEY, '').encode(), 'base64')
return cryptoManager().symDecrpyt(
return CryptoManager().symDecrpyt(
passkey, getUDSCookie(request)
) # recover as original unicode string
else: # No session, get from _session instead, this is an "client" REST request
return cryptoManager().symDecrpyt(request._cryptedpass, request._scrambler) # type: ignore
# No session, get from _session instead, this is an "client" REST request
return CryptoManager().symDecrpyt(
getattr(request, '_cryptedpass'), getattr(request, '_scrambler')
)
def webLogout(
@ -476,8 +490,6 @@ def webLogout(
)
else: # No user, redirect to /
return HttpResponseRedirect(reverse('page.login'))
except Exception:
raise
finally:
# Try to delete session
request.session.flush()
@ -513,14 +525,12 @@ def authLogLogin(
]
)
)
level = log.INFO if logStr == 'Logged in' else log.ERROR
level = log.LogLevel.INFO if logStr == 'Logged in' else log.LogLevel.ERROR
log.doLog(
authenticator,
level,
'user {} has {} from {} where os is {}'.format(
userName, logStr, request.ip, request.os.os.name
),
log.WEB,
f'user {userName} has {logStr} from {request.ip} where os is {request.os.os.name}',
log.LogSource.WEB,
)
try:
@ -529,12 +539,10 @@ def authLogLogin(
log.doLog(
user,
level,
'{} from {} where OS is {}'.format(
logStr, request.ip, request.os.os.name
),
log.WEB,
f'{logStr} from {request.ip} where OS is {request.os.os.name}',
log.LogSource.WEB,
)
except models.User.DoesNotExist:
except models.User.DoesNotExist: # pylint: disable=no-member
pass
@ -542,10 +550,8 @@ def authLogLogout(request: 'ExtendedHttpRequest') -> None:
if request.user:
log.doLog(
request.user.manager,
log.INFO,
'user {} has logged out from {}'.format(request.user.name, request.ip),
log.WEB,
)
log.doLog(
request.user, log.INFO, 'has logged out from {}'.format(request.ip), log.WEB
log.LogLevel.INFO,
f'user {request.user.name} has logged out from {request.ip}',
log.LogSource.WEB,
)
log.doLog(request.user, log.LogLevel.INFO, f'has logged out from {request.ip}', log.LogSource.WEB)

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# pylint: disable=unused-argument # this has a lot of "default" methods, so we need to ignore unused arguments most of the time
#
# Copyright (c) 2012-2020 Virtual Cable S.L.U.
# All rights reserved.
@ -29,17 +30,16 @@
"""
Base module for all authenticators
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import enum
import logging
from re import A
import typing
from django.utils.translation import gettext_noop as _
from django.urls import reverse
from uds.core import Module
from uds.core.module import Module
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
@ -65,6 +65,7 @@ class AuthenticationSuccess(enum.IntEnum):
OK = 1
REDIRECT = 2
class AuthenticationInternalUrl(enum.Enum):
"""
Enumeration for authentication success
@ -78,6 +79,7 @@ class AuthenticationInternalUrl(enum.Enum):
"""
return reverse(self.value)
class AuthenticationResult(typing.NamedTuple):
success: AuthenticationSuccess
url: typing.Optional[str] = None
@ -186,8 +188,8 @@ class Authenticator(Module):
# : If this authenticators casues a temporal block of an user on repeated login failures
blockUserOnLoginFailures: typing.ClassVar[bool] = True
from .user import User
from .group import Group
from .user import User # pylint: disable=import-outside-toplevel
from .group import Group # pylint: disable=import-outside-toplevel
# : The type of user provided, normally standard user will be enough.
# : This is here so if we need it in some case, we can write our own
@ -213,10 +215,12 @@ class Authenticator(Module):
@param environment: Environment for the authenticator
@param values: Values passed to element
"""
from uds.models import Authenticator as AuthenticatorModel
from uds.models import ( # pylint: disable=import-outside-toplevel
Authenticator as AuthenticatorModel,
)
self._dbAuth = dbAuth or AuthenticatorModel() # Fake dbAuth if not provided
super(Authenticator, self).__init__(environment, values)
super().__init__(environment, values)
self.initialize(values)
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
@ -249,9 +253,9 @@ class Authenticator(Module):
user param is a database user object
"""
from uds.core.auths.groups_manager import (
from uds.core.auths.groups_manager import ( # pylint: disable=import-outside-toplevel
GroupsManager,
) # pylint: disable=redefined-outer-name
)
if self.isExternalSource:
groupsManager = GroupsManager(self._dbAuth)
@ -268,7 +272,7 @@ class Authenticator(Module):
This method will allow us to know where to do redirection in case
we need to use callback for authentication
"""
from .auth import authCallbackUrl
from .auth import authCallbackUrl # pylint: disable=import-outside-toplevel
return authCallbackUrl(self.dbAuthenticator())
@ -276,7 +280,7 @@ class Authenticator(Module):
"""
Helper method to return info url for this authenticator
"""
from .auth import authInfoUrl
from .auth import authInfoUrl # pylint: disable=import-outside-toplevel
return authInfoUrl(self.dbAuthenticator())
@ -394,18 +398,26 @@ class Authenticator(Module):
"""
return FAILED_AUTH
def isAccesibleFrom(self, request: 'HttpRequest'):
def isAccesibleFrom(self, request: 'HttpRequest') -> bool:
"""
Used by the login interface to determine if the authenticator is visible on the login page.
"""
from uds.core.util.request import ExtendedHttpRequest
from uds.models import Authenticator as dbAuth
from uds.core.util.request import ( # pylint: disable=import-outside-toplevel
ExtendedHttpRequest,
)
from uds.models import ( # pylint: disable=import-outside-toplevel
Authenticator as dbAuth,
)
return self._dbAuth.state != dbAuth.DISABLED and self._dbAuth.validForIp(
typing.cast('ExtendedHttpRequest', request).ip
)
def transformUsername(self, username: str, request: 'ExtendedHttpRequest') -> str:
def transformUsername(
self,
username: str,
request: 'ExtendedHttpRequest',
) -> str:
"""
On login, this method get called so we can "transform" provided user name.
@ -462,7 +474,11 @@ class Authenticator(Module):
"""
return self.authenticate(username, credentials, groupsManager, request)
def logout(self, request: 'ExtendedHttpRequest', username: str) -> AuthenticationResult:
def logout(
self,
request: 'ExtendedHttpRequest',
username: str,
) -> AuthenticationResult:
"""
Invoked whenever an user logs out.
@ -491,7 +507,10 @@ class Authenticator(Module):
return SUCCESS_AUTH
def webLogoutHook(
self, username: str, request: 'HttpRequest', response: 'HttpResponse'
self,
username: str,
request: 'HttpRequest',
response: 'HttpResponse',
) -> None:
'''
Invoked on web logout of an user

View File

@ -40,4 +40,4 @@ if typing.TYPE_CHECKING:
class AuthsFactory(factory.ModuleFactory['Authenticator']):
pass
pass

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
@ -37,24 +37,18 @@ class AuthenticatorException(Exception):
Generic authentication exception
"""
pass
class InvalidUserException(AuthenticatorException):
"""
Invalid user specified. The user cant access the requested service
"""
pass
class InvalidAuthenticatorException(AuthenticatorException):
"""
Invalida authenticator has been specified
"""
pass
class Redirect(AuthenticatorException):
"""
@ -62,20 +56,14 @@ class Redirect(AuthenticatorException):
Used in authUrlCallback to indicate that redirect is needed
"""
pass
class Logout(AuthenticatorException):
"""
This exceptions redirects logouts an user and redirects to an url
"""
pass
class MFAError(AuthenticatorException):
"""
This exceptions indicates than an MFA error has ocurred
"""
pass

View File

@ -29,7 +29,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import re
import logging
@ -42,6 +42,7 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__)
class _LocalGrp(typing.NamedTuple):
name: str
group: 'Group'
@ -54,6 +55,7 @@ class _LocalGrp(typing.NamedTuple):
"""
return name.casefold() == self.name.casefold()
class GroupsManager:
"""
Manages registered groups for an specific authenticator.
@ -85,7 +87,9 @@ class GroupsManager:
self._dbAuthenticator = dbAuthenticator
# We just get active groups, inactive aren't visible to this class
self._groups = []
if dbAuthenticator.id: # If "fake" authenticator (that is, root user with no authenticator in fact)
if (
dbAuthenticator.id
): # If "fake" authenticator (that is, root user with no authenticator in fact)
for g in dbAuthenticator.groups.filter(state=State.ACTIVE, is_meta=False):
name = g.name.lower()
isPattern = name.find('pat:') == 0 # Is a pattern?
@ -93,7 +97,7 @@ class GroupsManager:
_LocalGrp(
name=name[4:] if isPattern else name,
group=Group(g),
is_pattern=isPattern
is_pattern=isPattern,
)
)
@ -127,7 +131,9 @@ class GroupsManager:
"""
returns the list of valid groups (:py:class:uds.core.auths.group.Group)
"""
from uds.models import Group as DBGroup
from uds.models import ( # pylint: disable=import-outside-toplevel
Group as DBGroup,
)
valid_id_list: typing.List[int] = []
for group in self._groups:
@ -139,7 +145,9 @@ class GroupsManager:
for db_group in DBGroup.objects.filter(
manager__id=self._dbAuthenticator.id, is_meta=True
): # @UndefinedVariable
gn = db_group.groups.filter(id__in=valid_id_list, state=State.ACTIVE).count()
gn = db_group.groups.filter(
id__in=valid_id_list, state=State.ACTIVE
).count()
if db_group.meta_if_any and gn > 0:
gn = db_group.groups.count()
if (
@ -183,7 +191,7 @@ class GroupsManager:
self.validate(n)
else:
for n in self._checkAllGroups(groupName):
self._groups[n] = self._groups[n]._replace(is_valid=True)
self._groups[n] = self._groups[n]._replace(is_valid=True)
def isValid(self, groupName: str) -> bool:
"""
@ -196,4 +204,4 @@ class GroupsManager:
return False
def __str__(self):
return "Groupsmanager: {0}".format(self._groups)
return f'Groupsmanager: {self._groups}'

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
@ -81,9 +81,9 @@ class User:
:note: Once obtained valid groups, it caches them until object removal.
"""
from uds.models.user import (
from uds.models.user import ( # pylint: disable=import-outside-toplevel
User as DBUser,
) # pylint: disable=redefined-outer-name
)
if self._groups is None:
if self._manager.isExternalSource:
@ -92,7 +92,6 @@ class User:
logger.debug(self._groups)
# This is just for updating "cached" data of this user, we only get real groups at login and at modify user operation
usr = DBUser.objects.get(pk=self._dbUser.id) # @UndefinedVariable
lst: typing.List[int] = []
usr.groups.set((g.dbGroup().id for g in self._groups if g.dbGroup().is_meta is False)) # type: ignore
else:
# From db

View File

@ -28,7 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
@ -70,11 +70,11 @@ class Environment:
{'mac' : UniqueMacGenerator, 'name' : UniqueNameGenerator } as argument.
"""
# Avoid circular imports
from uds.core.util.cache import Cache
from uds.core.util.storage import Storage
from uds.core.util.cache import Cache # pylint: disable=import-outside-toplevel
from uds.core.util.storage import Storage # pylint: disable=import-outside-toplevel
if idGenerators is None:
idGenerators = dict()
idGenerators = {}
self._key = uniqueKey
self._cache = Cache(uniqueKey)
self._storage = Storage(uniqueKey)
@ -105,7 +105,7 @@ class Environment:
@return: Generator for that id, or None if no generator for that id is found
"""
if not self._idGenerators or generatorId not in self._idGenerators:
raise Exception('No generator found for {}'.format(generatorId))
raise Exception(f'No generator found for {generatorId}')
return self._idGenerators[generatorId]
@property

View File

@ -31,22 +31,20 @@
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
class UDSException(Exception):
"""
Base class for all UDS exceptions
"""
pass
class ValidationError(UDSException):
"""
Exception used to indicate that the params assigned are invalid
"""
pass
class TransportError(UDSException):
"""
Exception used to indicate that the transport is not available
"""
pass

View File

@ -30,7 +30,7 @@
"""
UDS jobs related modules
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
Author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
from .job import Job
@ -45,6 +45,6 @@ def factory() -> 'JobsFactory':
"""
Returns a singleton to a jobs factory
"""
from .jobs_factory import JobsFactory # pylint: disable=redefined-outer-name
from .jobs_factory import JobsFactory # pylint: disable=import-outside-toplevel
return JobsFactory()

View File

@ -71,7 +71,7 @@ class DelayedTask(Environmentable):
"""
Utility method that allows to register a Delayedtask
"""
from .delayed_task_runner import DelayedTaskRunner
from .delayed_task_runner import DelayedTaskRunner # pylint: disable=import-outside-toplevel
if check and DelayedTaskRunner.runner().checkExists(tag):
return

View File

@ -42,7 +42,7 @@ from django.db import transaction, OperationalError
from django.db.models import Q
from uds.models import DelayedTask as DBDelayedTask
from uds.models import getSqlDatetime
from uds.core.util.model import getSqlDatetime
from uds.core.environment import Environment
from uds.core.util import singleton
@ -55,6 +55,7 @@ class DelayedTaskThread(threading.Thread):
"""
Class responsible of executing a delayed task in its own thread
"""
__slots__ = ('_taskInstance',)
_taskInstance: DelayedTask
@ -76,6 +77,7 @@ class DelayedTaskRunner(metaclass=singleton.Singleton):
"""
Delayed task runner class
"""
__slots__ = ()
granularity: typing.ClassVar[int] = 2 # we check for delayed tasks every "granularity" seconds
@ -105,9 +107,7 @@ class DelayedTaskRunner(metaclass=singleton.Singleton):
def executeOneDelayedTask(self) -> None:
now = getSqlDatetime()
filt = Q(execution_time__lt=now) | Q(
insert_date__gt=now + timedelta(seconds=30)
)
filt = Q(execution_time__lt=now) | Q(insert_date__gt=now + timedelta(seconds=30))
# If next execution is before now or last execution is in the future (clock changed on this server, we take that task as executable)
try:
with transaction.atomic(): # Encloses
@ -118,9 +118,7 @@ class DelayedTaskRunner(metaclass=singleton.Singleton):
.order_by('execution_time')[0] # type: ignore # Slicing is not supported by pylance right now
) # @UndefinedVariable
if task.insert_date > now + timedelta(seconds=30):
logger.warning(
'Executed %s due to insert_date being in the future!', task.type
)
logger.warning('Executed %s due to insert_date being in the future!', task.type)
taskInstanceDump = codecs.decode(task.instance.encode(), 'base64')
task.delete()
taskInstance = pickle.loads(taskInstanceDump) # nosec: controlled pickle
@ -186,18 +184,14 @@ class DelayedTaskRunner(metaclass=singleton.Singleton):
time.sleep(1) # Wait a bit before next try...
# If retries == 0, this is a big error
if retries == 0:
logger.error(
"Could not insert delayed task!!!! %s %s %s", instance, delay, tag
)
logger.error("Could not insert delayed task!!!! %s %s %s", instance, delay, tag)
return False
return True
def remove(self, tag: str) -> None:
try:
with transaction.atomic():
DBDelayedTask.objects.select_for_update().filter(
tag=tag
).delete() # @UndefinedVariable
DBDelayedTask.objects.select_for_update().filter(tag=tag).delete() # @UndefinedVariable
except Exception as e:
logger.exception('Exception removing a delayed task %s: %s', e.__class__, e)

View File

@ -31,7 +31,7 @@
import logging
import typing
from uds.core import Environmentable
from uds.core.environment import Environmentable
from uds.core.util.config import Config

View File

@ -47,9 +47,10 @@ class JobsFactory(factory.Factory['Job']):
"""
Ensures that uds core workers are correctly registered in database and in factory
"""
from uds.models import Scheduler, getSqlDatetime
from uds.core.util.state import State
from uds.core import workers
from uds.models import Scheduler # pylint: disable=import-outside-toplevel
from uds.core.util.model import getSqlDatetime # pylint: disable=import-outside-toplevel
from uds.core.util.state import State # pylint: disable=import-outside-toplevel
from uds.core import workers # pylint: disable=import-outside-toplevel
try:
logger.debug('Ensuring that jobs are registered inside database')

View File

@ -40,7 +40,8 @@ from datetime import timedelta
from django.db import transaction, DatabaseError, connections
from django.db.models import Q
from uds.models import Scheduler as DBScheduler, getSqlDatetime
from uds.models import Scheduler as DBScheduler
from uds.core.util.model import getSqlDatetime
from uds.core.util.state import State
from .jobs_factory import JobsFactory
@ -64,7 +65,7 @@ class JobThread(threading.Thread):
_freq: int
def __init__(self, jobInstance: 'Job', dbJob: DBScheduler) -> None:
super(JobThread, self).__init__()
super().__init__()
self._jobInstance = jobInstance
self._dbJobId = dbJob.id
self._freq = dbJob.frecuency
@ -186,8 +187,8 @@ class Scheduler:
# I have got some deadlock errors, but looking at that url, i found that it is not so abnormal
# logger.debug('Deadlock, no problem at all :-) (sounds hards, but really, no problem, will retry later :-) )')
raise DatabaseError(
'Database access problems. Retrying connection ({})'.format(e)
)
f'Database access problems. Retrying connection ({e})'
) from e
@staticmethod
def releaseOwnShedules() -> None:

Some files were not shown because too many files have changed in this diff Show More