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:
commit
afb1d2cafe
@ -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
11
SECURITY.md
Normal 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.
|
@ -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
|
||||
|
@ -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... :)
|
||||
|
@ -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',
|
||||
|
@ -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': '',
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
@ -1,2 +1,4 @@
|
||||
#!/bin/sh
|
||||
cp /UDSClient/UDSClient.desktop /usr/share/applications.mime
|
||||
chmod 755 /UDSClient/UDSClient
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
636
server/.pylintrc
Normal 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
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
||||
|
@ -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]))
|
||||
|
29
server/src/tests/fixtures/authenticators.py
vendored
29
server/src/tests/fixtures/authenticators.py
vendored
@ -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
|
||||
|
@ -79,7 +79,7 @@ class EmailNotifierTest(UDSTestCase):
|
||||
notifier.getInstance().notify(
|
||||
'Group',
|
||||
'Identificator',
|
||||
messaging.NotificationLevel.CRITICAL,
|
||||
messaging.LogLevel.CRITICAL,
|
||||
'Test message cañón',
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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],
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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 ''
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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()
|
||||
]
|
||||
),
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
]
|
||||
),
|
||||
|
@ -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'],
|
||||
|
@ -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')),
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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)}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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();'
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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}'
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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('<', '<').splitlines())
|
||||
if parameters.get('format') == 'html'
|
||||
else info
|
||||
'<br/>'.join(info.replace('<', '<').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):
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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}'
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -40,4 +40,4 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
|
||||
class AuthsFactory(factory.ModuleFactory['Authenticator']):
|
||||
pass
|
||||
pass
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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}'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user