mirror of
https://github.com/dkmstr/openuds.git
synced 2024-12-23 17:34:17 +03:00
adding initial tracking of individial sessions on user services
This commit is contained in:
parent
e5c8b9c763
commit
78a3216b51
@ -196,7 +196,7 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
|
||||
self.checkIdle()
|
||||
self.checkDeadLine()
|
||||
|
||||
time.sleep(1.3) # Sleeps between loop iterations
|
||||
time.sleep(1.22) # Sleeps between loop iterations
|
||||
|
||||
# If login was recognized...
|
||||
if self._loginInfo.logged_in:
|
||||
|
@ -132,7 +132,7 @@ class HTTPServerThread(threading.Thread):
|
||||
self._app = app
|
||||
|
||||
self.port = -1
|
||||
self.id = secrets.token_urlsafe(16)
|
||||
self.id = secrets.token_urlsafe(24)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
|
@ -33,8 +33,8 @@ import json
|
||||
import typing
|
||||
|
||||
import requests
|
||||
|
||||
from ..log import logger
|
||||
from udsactor import tools, types
|
||||
from udsactor.log import logger
|
||||
|
||||
# For avoid proxy on localhost connections
|
||||
NO_PROXY = {
|
||||
@ -42,55 +42,108 @@ NO_PROXY = {
|
||||
'https': None,
|
||||
}
|
||||
|
||||
class UDSActorClientPool:
|
||||
_clientUrl: typing.List[str]
|
||||
|
||||
class UDSActorClientPool(metaclass=tools.Singleton):
|
||||
_clients: typing.List[types.ClientInfo]
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._clientUrl = []
|
||||
self._clients = []
|
||||
|
||||
def _post(self, method: str, data: typing.MutableMapping[str, str], timeout=2) -> typing.List[requests.Response]:
|
||||
removables: typing.List[str] = []
|
||||
result: typing.List[typing.Any] = []
|
||||
for clientUrl in self._clientUrl:
|
||||
def _post(
|
||||
self,
|
||||
session_id: typing.Optional[str],
|
||||
method: str,
|
||||
data: typing.MutableMapping[str, str],
|
||||
timeout: int = 2,
|
||||
) -> typing.List[
|
||||
typing.Tuple[types.ClientInfo, typing.Optional[requests.Response]]
|
||||
]:
|
||||
result: typing.List[
|
||||
typing.Tuple[types.ClientInfo, typing.Optional[requests.Response]]
|
||||
] = []
|
||||
for client in self._clients:
|
||||
# Skip if session id is provided but does not match
|
||||
if session_id and client.session_id != session_id:
|
||||
continue
|
||||
clientUrl = client.url
|
||||
try:
|
||||
result.append(requests.post(clientUrl + '/' + method, data=json.dumps(data), verify=False, timeout=timeout, proxies=NO_PROXY))
|
||||
result.append(
|
||||
(
|
||||
client,
|
||||
requests.post(
|
||||
clientUrl + '/' + method,
|
||||
data=json.dumps(data),
|
||||
verify=False,
|
||||
timeout=timeout,
|
||||
proxies=NO_PROXY, # type: ignore
|
||||
),
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
# If cannot request to a clientUrl, remove it from list
|
||||
logger.info('Could not connect with client %s: %s. Removed from registry.', e, clientUrl)
|
||||
removables.append(clientUrl)
|
||||
|
||||
# Remove failed connections
|
||||
for clientUrl in removables:
|
||||
self.unregister(clientUrl)
|
||||
logger.info(
|
||||
'Could not connect with client %s: %s. ',
|
||||
e,
|
||||
clientUrl,
|
||||
)
|
||||
result.append((client, None))
|
||||
|
||||
return result
|
||||
|
||||
def register(self, clientUrl: str) -> None:
|
||||
@property
|
||||
def clients(self) -> typing.List[types.ClientInfo]:
|
||||
return self._clients
|
||||
|
||||
def register(self, client_url: str) -> None:
|
||||
# Remove first if exists, to avoid duplicates
|
||||
self.unregister(clientUrl)
|
||||
self.unregister(client_url)
|
||||
# And add it again
|
||||
self._clientUrl.append(clientUrl)
|
||||
self._clients.append(types.ClientInfo(client_url, ''))
|
||||
|
||||
def unregister(self, clientUrl: str) -> None:
|
||||
self._clientUrl = list((i for i in self._clientUrl if i != clientUrl))
|
||||
def set_session_id(self, client_url: str, session_id: typing.Optional[str]) -> None:
|
||||
"""Set the session id for a client
|
||||
|
||||
def executeScript(self, script: str) -> None:
|
||||
self._post('script', {'script': script}, timeout=30)
|
||||
Args:
|
||||
clientUrl (str): _description_
|
||||
session_id (str): _description_
|
||||
"""
|
||||
for client in self._clients:
|
||||
if client.url == client_url:
|
||||
# remove existing client from list, create a new one and insert it
|
||||
self._clients.remove(client)
|
||||
self._clients.append(types.ClientInfo(client_url, session_id or ''))
|
||||
break
|
||||
|
||||
def logout(self) -> None:
|
||||
self._post('logout', {})
|
||||
def unregister(self, client_url: str) -> None:
|
||||
# remove client url from array if found
|
||||
for i, client in enumerate(self._clients):
|
||||
if client.url == client_url:
|
||||
self._clients.pop(i)
|
||||
return
|
||||
|
||||
def message(self, message: str) -> None:
|
||||
self._post('message', {'message': message})
|
||||
def executeScript(self, session_id: typing.Optional[str], script: str) -> None:
|
||||
self._post(session_id, 'script', {'script': script}, timeout=30)
|
||||
|
||||
def ping(self) -> bool:
|
||||
if not self._clientUrl:
|
||||
return True # No clients, ping ok
|
||||
self._post('ping', {}, timeout=1)
|
||||
return bool(self._clientUrl) # There was clients, but they are now lost!!!
|
||||
def logout(self, session_id: typing.Optional[str]) -> None:
|
||||
self._post(session_id, 'logout', {})
|
||||
|
||||
def screenshot(self) -> typing.Optional[str]: # Screenshot are returned as base64
|
||||
for r in self._post('screenshot', {}, timeout=3):
|
||||
def message(self, session_id: typing.Optional[str], message: str) -> None:
|
||||
self._post(session_id, 'message', {'message': message})
|
||||
|
||||
def lost_clients(
|
||||
self,
|
||||
session_id: typing.Optional[str] = None,
|
||||
) -> typing.Iterable[types.ClientInfo]: # returns the list of "lost" clients
|
||||
# Port ping to every client
|
||||
for i in self._post(session_id, 'ping', {}, timeout=1):
|
||||
if i[1] is None:
|
||||
yield i[0]
|
||||
|
||||
def screenshot(
|
||||
self, session_id: typing.Optional[str]
|
||||
) -> typing.Optional[str]: # Screenshot are returned as base64
|
||||
for client, r in self._post(session_id, 'screenshot', {}, timeout=3):
|
||||
if not r:
|
||||
continue # Missing client, so we ignore it
|
||||
try:
|
||||
return r.json()['result']
|
||||
except Exception:
|
||||
|
@ -30,19 +30,23 @@
|
||||
'''
|
||||
import typing
|
||||
|
||||
from . import handler
|
||||
from udsactor.http import handler, clients_pool
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..service import CommonService
|
||||
from udsactor.service import CommonService
|
||||
|
||||
class LocalProvider(handler.Handler):
|
||||
|
||||
def post_login(self) -> typing.Any:
|
||||
result = self._service.login(self._params['username'], self._params['session_type'])
|
||||
# if callback_url is provided, record it in the clients pool
|
||||
if 'callback_url' in self._params and result.session_id:
|
||||
# If no session id is returned, then no login is acounted for
|
||||
clients_pool.UDSActorClientPool().set_session_id(self._params['callback_url'], result.session_id)
|
||||
return result._asdict()
|
||||
|
||||
def post_logout(self) -> typing.Any:
|
||||
self._service.logout(self._params['username'])
|
||||
self._service.logout(self._params['username'], self._params['session_type'], self._params['session_id'])
|
||||
return 'ok'
|
||||
|
||||
def post_ping(self) -> typing.Any:
|
||||
|
@ -36,8 +36,8 @@ import typing
|
||||
|
||||
import requests
|
||||
|
||||
from . import types
|
||||
from .version import VERSION
|
||||
from udsactor import types, tools
|
||||
from udsactor.version import VERSION
|
||||
|
||||
# Default public listen port
|
||||
LISTEN_PORT = 43910
|
||||
@ -90,9 +90,9 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
Base for remote api accesses
|
||||
"""
|
||||
|
||||
_host: str
|
||||
_validateCert: bool
|
||||
_url: str
|
||||
_host: str = ''
|
||||
_validateCert: bool = True
|
||||
_url: str = ''
|
||||
|
||||
def __init__(self, host: str, validateCert: bool) -> None:
|
||||
self._host = host
|
||||
@ -113,7 +113,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
'User-Agent': 'UDS Actor v{}'.format(VERSION),
|
||||
}
|
||||
|
||||
def _apiURL(self, method: str) -> str:
|
||||
def _api_url(self, method: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def _doPost(
|
||||
@ -126,7 +126,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
headers = headers or self._headers
|
||||
try:
|
||||
result = requests.post(
|
||||
self._apiURL(method),
|
||||
self._api_url(method),
|
||||
data=json.dumps(payLoad),
|
||||
headers=headers,
|
||||
verify=self._validateCert,
|
||||
@ -157,7 +157,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
# UDS Broker API access
|
||||
#
|
||||
class UDSServerApi(UDSApi):
|
||||
def _apiURL(self, method: str) -> str:
|
||||
def _api_url(self, method: str) -> str:
|
||||
return self._url + 'actor/v3/' + method
|
||||
|
||||
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
|
||||
@ -225,7 +225,7 @@ class UDSServerApi(UDSApi):
|
||||
headers['X-Auth-Token'] = result.json()['token']
|
||||
|
||||
result = requests.post(
|
||||
self._apiURL('register'),
|
||||
self._api_url('register'),
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
verify=self._validateCert,
|
||||
@ -323,7 +323,7 @@ class UDSServerApi(UDSApi):
|
||||
actor_type: typing.Optional[str],
|
||||
token: str,
|
||||
username: str,
|
||||
sessionType: str,
|
||||
session_type: str,
|
||||
interfaces: typing.Iterable[types.InterfaceInfoType],
|
||||
secret: typing.Optional[str],
|
||||
) -> types.LoginResultInfoType:
|
||||
@ -336,7 +336,7 @@ class UDSServerApi(UDSApi):
|
||||
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
|
||||
'token': token,
|
||||
'username': username,
|
||||
'session_type': sessionType,
|
||||
'session_type': session_type,
|
||||
'secret': secret or '',
|
||||
}
|
||||
result = self._doPost('login', payload)
|
||||
@ -345,7 +345,7 @@ class UDSServerApi(UDSApi):
|
||||
hostname=result['hostname'],
|
||||
dead_line=result['dead_line'],
|
||||
max_idle=result['max_idle'],
|
||||
session_id=result['session_id'],
|
||||
session_id=result.get('session_id', ''),
|
||||
)
|
||||
|
||||
def logout(
|
||||
@ -353,7 +353,7 @@ class UDSServerApi(UDSApi):
|
||||
actor_type: typing.Optional[str],
|
||||
token: str,
|
||||
username: str,
|
||||
session_id: typing.Optional[str],
|
||||
session_id: str,
|
||||
session_type: str,
|
||||
interfaces: typing.Iterable[types.InterfaceInfoType],
|
||||
secret: typing.Optional[str],
|
||||
@ -366,7 +366,7 @@ class UDSServerApi(UDSApi):
|
||||
'token': token,
|
||||
'username': username,
|
||||
'session_type': session_type,
|
||||
'session_id': session_id or '',
|
||||
'session_id': session_id,
|
||||
'secret': secret or '',
|
||||
}
|
||||
return self._doPost('logout', payload) # Can be 'ok' or 'notified'
|
||||
@ -385,13 +385,17 @@ class UDSServerApi(UDSApi):
|
||||
return self._doPost('test', payLoad) == 'ok'
|
||||
|
||||
|
||||
class UDSClientApi(UDSApi):
|
||||
class UDSClientApi(UDSApi, metaclass=tools.Singleton):
|
||||
_session_id: str = ''
|
||||
_callback_url: str = ''
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__('127.0.0.1:{}'.format(LISTEN_PORT), False)
|
||||
# Override base url
|
||||
|
||||
# Replace base url
|
||||
self._url = "https://{}/ui/".format(self._host)
|
||||
|
||||
def _apiURL(self, method: str) -> str:
|
||||
def _api_url(self, method: str) -> str:
|
||||
return self._url + method
|
||||
|
||||
def post(
|
||||
@ -401,13 +405,15 @@ class UDSClientApi(UDSApi):
|
||||
) -> typing.Any:
|
||||
return self._doPost(method=method, payLoad=payLoad, disableProxy=True)
|
||||
|
||||
def register(self, callbackUrl: str) -> None:
|
||||
payLoad = {'callback_url': callbackUrl}
|
||||
def register(self, callback_url: str) -> None:
|
||||
self._callback_url = callback_url
|
||||
payLoad = {'callback_url': callback_url}
|
||||
self.post('register', payLoad)
|
||||
|
||||
def unregister(self, callbackUrl: str) -> None:
|
||||
payLoad = {'callback_url': callbackUrl}
|
||||
def unregister(self, callback_url: str) -> None:
|
||||
payLoad = {'callback_url': callback_url}
|
||||
self.post('unregister', payLoad)
|
||||
self._callback_url = ''
|
||||
|
||||
def login(
|
||||
self, username: str, sessionType: typing.Optional[str] = None
|
||||
@ -415,20 +421,26 @@ class UDSClientApi(UDSApi):
|
||||
payLoad = {
|
||||
'username': username,
|
||||
'session_type': sessionType or UNKNOWN,
|
||||
'callback_url': self._callback_url, # So we identify ourselves
|
||||
}
|
||||
result = self.post('login', payLoad)
|
||||
return types.LoginResultInfoType(
|
||||
res = types.LoginResultInfoType(
|
||||
ip=result['ip'],
|
||||
hostname=result['hostname'],
|
||||
dead_line=result['dead_line'],
|
||||
max_idle=result['max_idle'],
|
||||
session_id=result['session_id'],
|
||||
)
|
||||
# Store session id for future use
|
||||
self._session_id = res.session_id or ''
|
||||
return res
|
||||
|
||||
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
|
||||
payLoad = {
|
||||
'username': username,
|
||||
'session_type': sessionType or UNKNOWN
|
||||
'session_type': sessionType or UNKNOWN,
|
||||
'callback_url': self._callback_url, # So we identify ourselves
|
||||
'session_id': self._session_id, # We now know the session id, provided on login
|
||||
}
|
||||
self.post('logout', payLoad)
|
||||
|
||||
|
@ -36,13 +36,13 @@ import secrets
|
||||
import subprocess
|
||||
import typing
|
||||
|
||||
from . import platform
|
||||
from . import rest
|
||||
from . import types
|
||||
from . import tools
|
||||
from udsactor import platform
|
||||
from udsactor import rest
|
||||
from udsactor import types
|
||||
from udsactor import tools
|
||||
|
||||
from .log import logger, DEBUG, INFO, ERROR, FATAL
|
||||
from .http import clients_pool, server, cert
|
||||
from udsactor.log import logger, DEBUG, INFO, ERROR, FATAL
|
||||
from udsactor.http import clients_pool, server, cert
|
||||
|
||||
# def setup() -> None:
|
||||
# cfg = platform.store.readConfig()
|
||||
@ -60,15 +60,12 @@ from .http import clients_pool, server, cert
|
||||
class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
_isAlive: bool = True
|
||||
_rebootRequested: bool = False
|
||||
_loggedIn: bool = False
|
||||
_initialized: bool = False
|
||||
|
||||
_cfg: types.ActorConfigurationType
|
||||
_api: rest.UDSServerApi
|
||||
_interfaces: typing.List[types.InterfaceInfoType]
|
||||
_secret: str
|
||||
_certificate: types.CertificateInfoType
|
||||
_clientsPool: clients_pool.UDSActorClientPool
|
||||
_http: typing.Optional[server.HTTPServerThread]
|
||||
|
||||
@staticmethod
|
||||
@ -324,7 +321,11 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# Only removes master token for managed machines (will need it on next client execution)
|
||||
# For unmanaged, if alias is present, replace master token with it
|
||||
master_token = None if self.isManaged() else (initResult.alias_token or self._cfg.master_token)
|
||||
master_token = (
|
||||
None
|
||||
if self.isManaged()
|
||||
else (initResult.alias_token or self._cfg.master_token)
|
||||
)
|
||||
self._cfg = self._cfg._replace(
|
||||
master_token=master_token,
|
||||
own_token=initResult.own_token,
|
||||
@ -372,19 +373,23 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._http.stop()
|
||||
|
||||
# If logged in, notify UDS of logout (daemon stoped = no control = logout)
|
||||
if self._loggedIn and self._cfg.own_token:
|
||||
self._loggedIn = False
|
||||
try:
|
||||
self._api.logout(
|
||||
self._cfg.actorType,
|
||||
self._cfg.own_token,
|
||||
'',
|
||||
'',
|
||||
self._interfaces,
|
||||
self._secret,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Error notifying final logout to UDS: %s', e)
|
||||
# For every connected client...
|
||||
if self._cfg.own_token:
|
||||
for client in clients_pool.UDSActorClientPool().clients:
|
||||
if client.session_id:
|
||||
try:
|
||||
self._api.logout(
|
||||
self._cfg.actorType,
|
||||
self._cfg.own_token,
|
||||
'',
|
||||
client.session_id
|
||||
or 'stop', # If no session id, pass "stop"
|
||||
'',
|
||||
self._interfaces,
|
||||
self._secret,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Error notifying final logout to UDS: %s', e)
|
||||
|
||||
self.notifyStop()
|
||||
|
||||
@ -466,8 +471,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self.checkIpsChanged()
|
||||
|
||||
# Now check if every registered client is already there (if logged in OFC)
|
||||
if self._loggedIn and not self._clientsPool.ping():
|
||||
self.logout('client_unavailable')
|
||||
for lost_client in clients_pool.UDSActorClientPool().lost_clients():
|
||||
logger.info('Lost client: {}'.format(lost_client))
|
||||
self.logout('client_unavailable', '', lost_client.session_id or '')
|
||||
except Exception as e:
|
||||
logger.error('Exception on main service loop: %s', e)
|
||||
|
||||
@ -488,10 +494,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self, username: str, sessionType: typing.Optional[str] = None
|
||||
) -> types.LoginResultInfoType:
|
||||
result = types.LoginResultInfoType(
|
||||
ip='', hostname='', dead_line=None, max_idle=None
|
||||
ip='', hostname='', dead_line=None, max_idle=None, session_id=None
|
||||
)
|
||||
self._loggedIn = True
|
||||
|
||||
master_token = None
|
||||
secret = None
|
||||
# If unmanaged, do initialization now, because we don't know before this
|
||||
@ -517,16 +521,22 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
secret,
|
||||
)
|
||||
|
||||
script = platform.store.invokeScriptOnLogin()
|
||||
if script:
|
||||
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
|
||||
self.execute(script, 'Logon')
|
||||
if (
|
||||
result.session_id
|
||||
): # If logged in, process it. client_pool will take account of login response to client and session
|
||||
script = platform.store.invokeScriptOnLogin()
|
||||
if script:
|
||||
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
|
||||
self.execute(script, 'Logon')
|
||||
|
||||
return result
|
||||
|
||||
def logout(self, username: str, sessionType: typing.Optional[str] = None) -> None:
|
||||
self._loggedIn = False
|
||||
|
||||
def logout(
|
||||
self,
|
||||
username: str,
|
||||
session_type: typing.Optional[str],
|
||||
session_id: typing.Optional[str],
|
||||
) -> None:
|
||||
master_token = self._cfg.master_token
|
||||
|
||||
# Own token will not be set if UDS did not assigned the initialized VM to an user
|
||||
@ -539,13 +549,16 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._cfg.actorType,
|
||||
token,
|
||||
username,
|
||||
sessionType or '',
|
||||
session_id or '',
|
||||
session_type or '',
|
||||
self._interfaces,
|
||||
self._secret,
|
||||
)
|
||||
!= 'ok'
|
||||
):
|
||||
logger.info('Logout from %s ignored as required by uds broker', username)
|
||||
logger.info(
|
||||
'Logout from %s ignored as required by uds broker', username
|
||||
)
|
||||
return
|
||||
|
||||
self.onLogout(username)
|
||||
|
@ -52,6 +52,26 @@ class ScriptExecutorThread(threading.Thread):
|
||||
logger.error('Error executing script: {}'.format(e))
|
||||
logger.exception()
|
||||
|
||||
class Singleton(type):
|
||||
'''
|
||||
Metaclass for singleton pattern
|
||||
Usage:
|
||||
|
||||
class MyClass(metaclass=Singleton):
|
||||
...
|
||||
'''
|
||||
_instance: typing.Optional[typing.Any]
|
||||
|
||||
# We use __init__ so we customise the created class from this metaclass
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self._instance = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __call__(self, *args, **kwargs) -> typing.Any:
|
||||
if self._instance is None:
|
||||
self._instance = super().__call__(*args, **kwargs)
|
||||
return self._instance
|
||||
|
||||
|
||||
# Convert "X.X.X.X/X" to ipaddress.IPv4Network
|
||||
def strToNoIPV4Network(net: typing.Optional[str]) -> typing.Optional[ipaddress.IPv4Network]:
|
||||
|
@ -62,7 +62,11 @@ class LoginResultInfoType(typing.NamedTuple):
|
||||
|
||||
@property
|
||||
def logged_in(self) -> bool:
|
||||
return self.hostname != '' or self.ip != ''
|
||||
return bool(self.session_id)
|
||||
|
||||
class ClientInfo(typing.NamedTuple):
|
||||
url: str
|
||||
session_id: str
|
||||
|
||||
class CertificateInfoType(typing.NamedTuple):
|
||||
private_key: str
|
||||
|
@ -253,10 +253,14 @@ class Initialize(ActorV3Action):
|
||||
# Retrieve real service from token alias
|
||||
service = ServiceTokenAlias.objects.get(alias=token).service
|
||||
# If not found, try to locate on service table
|
||||
if service is None: # Not on alias token, try to locate on Service table
|
||||
if (
|
||||
service is None
|
||||
): # Not on alias token, try to locate on Service table
|
||||
service = Service.objects.get(token=token)
|
||||
# And create a new alias for it, and save
|
||||
alias_token = cryptoManager().randomString() # fix alias with new token
|
||||
alias_token = (
|
||||
cryptoManager().randomString()
|
||||
) # fix alias with new token
|
||||
service.aliases.create(alias=alias_token)
|
||||
|
||||
# Locate an userService that belongs to this service and which
|
||||
@ -288,7 +292,13 @@ class Initialize(ActorV3Action):
|
||||
except Exception as e:
|
||||
logger.info('Unmanaged host request: %s, %s', self._params, e)
|
||||
return ActorV3Action.actorResult(
|
||||
{'own_token': None, 'max_idle': None, 'unique_id': None, 'os': None, 'alias': None}
|
||||
{
|
||||
'own_token': None,
|
||||
'max_idle': None,
|
||||
'unique_id': None,
|
||||
'os': None,
|
||||
'alias': None,
|
||||
}
|
||||
)
|
||||
|
||||
# Managed by UDS, get initialization data from osmanager and return it
|
||||
@ -508,6 +518,7 @@ class Login(LoginLogout):
|
||||
isManaged = self._params.get('type') != UNMANAGED
|
||||
ip = hostname = ''
|
||||
deadLine = maxIdle = None
|
||||
session_id = ''
|
||||
|
||||
logger.debug('Login Args: %s, Params: %s', self._args, self._params)
|
||||
|
||||
@ -522,6 +533,9 @@ class Login(LoginLogout):
|
||||
logger.debug('Max idle: %s', maxIdle)
|
||||
|
||||
ip, hostname = userService.getConnectionSource()
|
||||
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():
|
||||
@ -537,7 +551,13 @@ class Login(LoginLogout):
|
||||
self.notifyService(isLogin=True)
|
||||
|
||||
return ActorV3Action.actorResult(
|
||||
{'ip': ip, 'hostname': hostname, 'dead_line': deadLine, 'max_idle': maxIdle}
|
||||
{
|
||||
'ip': ip,
|
||||
'hostname': hostname,
|
||||
'dead_line': deadLine,
|
||||
'max_idle': maxIdle,
|
||||
'session_id': session_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -549,13 +569,20 @@ class Logout(LoginLogout):
|
||||
name = 'logout'
|
||||
|
||||
@staticmethod
|
||||
def process_logout(userService: UserService, username: 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()
|
||||
|
||||
# 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.)
|
||||
@ -572,13 +599,21 @@ class Logout(LoginLogout):
|
||||
|
||||
logger.debug('Args: %s, Params: %s', self._args, self._params)
|
||||
try:
|
||||
userService: UserService = self.getUserService()
|
||||
Logout.process_logout(userService, self._params.get('username') or '')
|
||||
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...
|
||||
if isManaged:
|
||||
raise
|
||||
self.notifyService(isLogin=False) # Logout notification
|
||||
return ActorV3Action.actorResult('notified') # Result is that we have not processed the logout in fact, but notified the service
|
||||
return ActorV3Action.actorResult(
|
||||
'notified'
|
||||
) # Result is that we have not processed the logout in fact, but notified the service
|
||||
|
||||
return ActorV3Action.actorResult('ok')
|
||||
|
||||
@ -707,7 +742,7 @@ class Unmanaged(ActorV3Action):
|
||||
if validId:
|
||||
# If id is assigned to an user service, notify "logout" to it
|
||||
if userService:
|
||||
Logout.process_logout(userService, 'init')
|
||||
Logout.process_logout(userService, 'init', '')
|
||||
else:
|
||||
# If it is not assgined to an user service, notify service
|
||||
service.notifyInitialization(validId)
|
||||
|
@ -33,6 +33,7 @@ import hashlib
|
||||
import array
|
||||
import uuid
|
||||
import codecs
|
||||
import datetime
|
||||
import struct
|
||||
import re
|
||||
import random
|
||||
@ -162,7 +163,9 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
toDecode = decryptor.update(text) + decryptor.finalize()
|
||||
return toDecode[4 : 4 + struct.unpack('>i', toDecode[:4])[0]]
|
||||
|
||||
def xor(self, value: typing.Union[str, bytes], key: typing.Union[str, bytes]) -> bytes:
|
||||
def xor(
|
||||
self, value: typing.Union[str, bytes], key: typing.Union[str, bytes]
|
||||
) -> bytes:
|
||||
if not key:
|
||||
return b'' # Protect against division by cero
|
||||
|
||||
@ -172,9 +175,13 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
key = key.encode('utf-8')
|
||||
mult = len(value) // len(key) + 1
|
||||
value_array = array.array('B', value)
|
||||
key_array = array.array('B', key * mult) # Ensure key array is at least as long as value_array
|
||||
key_array = array.array(
|
||||
'B', key * mult
|
||||
) # Ensure key array is at least as long as value_array
|
||||
# We must return binary in xor, because result is in fact binary
|
||||
return array.array('B', (value_array[i] ^ key_array[i] for i in range(len(value_array)))).tobytes()
|
||||
return array.array(
|
||||
'B', (value_array[i] ^ key_array[i] for i in range(len(value_array)))
|
||||
).tobytes()
|
||||
|
||||
def symCrypt(
|
||||
self, text: typing.Union[str, bytes], key: typing.Union[str, bytes]
|
||||
@ -248,7 +255,7 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
return not hash
|
||||
|
||||
if hash[:8] == '{SHA256}':
|
||||
return str(hashlib.sha3_256(value).hexdigest()) == hash[8:]
|
||||
return hashlib.sha3_256(value).hexdigest() == hash[8:]
|
||||
else: # Old sha1
|
||||
return hash == str(hashlib.sha1(value).hexdigest())
|
||||
|
||||
@ -272,3 +279,11 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
def randomString(self, length: int = 40, digits: bool = True) -> str:
|
||||
base = string.ascii_letters + (string.digits if digits else '')
|
||||
return ''.join(random.SystemRandom().choices(base, k=length))
|
||||
|
||||
def unique(self) -> str:
|
||||
return hashlib.sha3_256(
|
||||
(
|
||||
self.randomString(24, True)
|
||||
+ datetime.datetime.now().strftime('%H%M%S%f')
|
||||
).encode()
|
||||
).hexdigest()
|
||||
|
@ -1,129 +1,222 @@
|
||||
# Generated by Django 4.0.3 on 2022-07-05 12:43
|
||||
# Generated by Django 4.1 on 2022-08-07 13:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uds.models.notifications
|
||||
import uds.models.user_service_session
|
||||
import uds.models.util
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uds', '0043_auto_20220704_2120'),
|
||||
("uds", "0043_auto_20220704_2120"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Notification',
|
||||
name="Notification",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('stamp', models.DateTimeField(auto_now_add=True)),
|
||||
('group', models.CharField(db_index=True, max_length=128)),
|
||||
('identificator', models.CharField(db_index=True, max_length=128)),
|
||||
('level', models.PositiveSmallIntegerField()),
|
||||
('message', models.TextField()),
|
||||
('processed', models.BooleanField(default=False)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("stamp", models.DateTimeField(auto_now_add=True)),
|
||||
("group", models.CharField(db_index=True, max_length=128)),
|
||||
("identificator", models.CharField(db_index=True, max_length=128)),
|
||||
("level", models.PositiveSmallIntegerField()),
|
||||
("message", models.TextField()),
|
||||
("processed", models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'db_table': 'uds_notification',
|
||||
"db_table": "uds_notification",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Notifier',
|
||||
name="Notifier",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('uuid', models.CharField(default=None, max_length=50, null=True, unique=True)),
|
||||
('data_type', models.CharField(max_length=128)),
|
||||
('data', models.TextField(default='')),
|
||||
('name', models.CharField(default='', max_length=128)),
|
||||
('comments', models.CharField(default='', max_length=256)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('level', models.PositiveSmallIntegerField(default=uds.models.notifications.NotificationLevel['ERROR'])),
|
||||
('tags', models.ManyToManyField(to='uds.tag')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uuid",
|
||||
models.CharField(
|
||||
default=None, max_length=50, null=True, unique=True
|
||||
),
|
||||
),
|
||||
("data_type", models.CharField(max_length=128)),
|
||||
("data", models.TextField(default="")),
|
||||
("name", models.CharField(default="", max_length=128)),
|
||||
("comments", models.CharField(default="", max_length=256)),
|
||||
("enabled", models.BooleanField(default=True)),
|
||||
(
|
||||
"level",
|
||||
models.PositiveSmallIntegerField(
|
||||
default=uds.models.notifications.NotificationLevel["ERROR"]
|
||||
),
|
||||
),
|
||||
("tags", models.ManyToManyField(to="uds.tag")),
|
||||
],
|
||||
options={
|
||||
'db_table': 'uds_notify_prov',
|
||||
"db_table": "uds_notify_prov",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ServiceTokenAlias',
|
||||
name="ServiceTokenAlias",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('alias', models.CharField(max_length=64, unique=True)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("alias", models.CharField(max_length=64, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='authenticator',
|
||||
name='visible',
|
||||
migrations.CreateModel(
|
||||
name="UserServiceSession",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"session_id",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
db_index=True,
|
||||
default=uds.models.user_service_session._session_id_generator,
|
||||
max_length=128,
|
||||
),
|
||||
),
|
||||
("start", models.DateTimeField(default=uds.models.util.getSqlDatetime)),
|
||||
("end", models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
"db_table": "uds__user_service_session",
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='service',
|
||||
name='proxy',
|
||||
model_name="authenticator",
|
||||
name="visible",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='transport',
|
||||
name='nets_positive',
|
||||
model_name="service",
|
||||
name="proxy",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="transport",
|
||||
name="nets_positive",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="userservice",
|
||||
name="cluster_node",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authenticator',
|
||||
name='net_filtering',
|
||||
field=models.CharField(db_index=True, default='n', max_length=1),
|
||||
model_name="authenticator",
|
||||
name="net_filtering",
|
||||
field=models.CharField(db_index=True, default="n", max_length=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='authenticator',
|
||||
name='state',
|
||||
field=models.CharField(db_index=True, default='v', max_length=1),
|
||||
model_name="authenticator",
|
||||
name="state",
|
||||
field=models.CharField(db_index=True, default="v", max_length=1),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='config',
|
||||
name='help',
|
||||
field=models.CharField(default='', max_length=256),
|
||||
model_name="config",
|
||||
name="help",
|
||||
field=models.CharField(default="", max_length=256),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='metapool',
|
||||
name='ha_policy',
|
||||
model_name="metapool",
|
||||
name="ha_policy",
|
||||
field=models.SmallIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='network',
|
||||
name='authenticators',
|
||||
field=models.ManyToManyField(db_table='uds_net_auths', related_name='networks', to='uds.authenticator'),
|
||||
model_name="network",
|
||||
name="authenticators",
|
||||
field=models.ManyToManyField(
|
||||
db_table="uds_net_auths",
|
||||
related_name="networks",
|
||||
to="uds.authenticator",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='max_services_count_type',
|
||||
model_name="service",
|
||||
name="max_services_count_type",
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transport',
|
||||
name='net_filtering',
|
||||
field=models.CharField(db_index=True, default='n', max_length=1),
|
||||
model_name="transport",
|
||||
name="net_filtering",
|
||||
field=models.CharField(db_index=True, default="n", max_length=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='service',
|
||||
name='token',
|
||||
field=models.CharField(blank=True, default=None, max_length=64, null=True, unique=True),
|
||||
model_name="service",
|
||||
name="token",
|
||||
field=models.CharField(
|
||||
blank=True, default=None, max_length=64, null=True, unique=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tunneltoken',
|
||||
name='ip',
|
||||
model_name="tunneltoken",
|
||||
name="ip",
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tunneltoken',
|
||||
name='ip_from',
|
||||
model_name="tunneltoken",
|
||||
name="ip_from",
|
||||
field=models.CharField(max_length=128),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userservice',
|
||||
name='src_ip',
|
||||
field=models.CharField(default='', max_length=128),
|
||||
model_name="userservice",
|
||||
name="src_ip",
|
||||
field=models.CharField(default="", max_length=128),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Proxy',
|
||||
name="Proxy",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='servicetokenalias',
|
||||
name='service',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aliases', to='uds.service'),
|
||||
model_name="userservicesession",
|
||||
name="user_service",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="sessions",
|
||||
to="uds.userservice",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="servicetokenalias",
|
||||
name="service",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="aliases",
|
||||
to="uds.service",
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="userservicesession",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("session_id", "user_service"), name="u_session_userservice"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -63,8 +63,10 @@ from .service_pool import ServicePool # New name
|
||||
from .meta_pool import MetaPool, MetaPoolMember
|
||||
from .service_pool_group import ServicePoolGroup
|
||||
from .service_pool_publication import ServicePoolPublication, ServicePoolPublicationChangelog
|
||||
|
||||
from .user_service import UserService
|
||||
from .user_service_property import UserServiceProperty
|
||||
from .user_service_session import UserServiceSession
|
||||
|
||||
# Especific log information for an user service
|
||||
from .log import Log
|
||||
|
@ -36,17 +36,17 @@ import typing
|
||||
from django.db import models
|
||||
from django.db.models import signals
|
||||
|
||||
from uds.core.managers import cryptoManager
|
||||
from uds.core.environment import Environment
|
||||
from uds.core.util import log
|
||||
from uds.core.util import unique
|
||||
from uds.core.util import log, unique
|
||||
from uds.core.util.state import State
|
||||
|
||||
from .uuid_model import UUIDModel
|
||||
from .service_pool import ServicePool
|
||||
from .service_pool_publication import ServicePoolPublication
|
||||
from .user import User
|
||||
from .util import NEVER
|
||||
from .util import getSqlDatetime
|
||||
from uds.models.uuid_model import UUIDModel
|
||||
from uds.models.service_pool import ServicePool
|
||||
from uds.models.service_pool_publication import ServicePoolPublication
|
||||
from uds.models.user import User
|
||||
from uds.models.util import NEVER
|
||||
from uds.models.util import getSqlDatetime
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
@ -57,12 +57,12 @@ if typing.TYPE_CHECKING:
|
||||
ServicePool,
|
||||
ServicePoolPublication,
|
||||
UserServiceProperty,
|
||||
UserServiceSession,
|
||||
AccountUsage,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserService(UUIDModel): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
This is the base model for assigned user service and cached user services.
|
||||
@ -115,13 +115,10 @@ class UserService(UUIDModel): # pylint: disable=too-many-public-methods
|
||||
src_hostname = models.CharField(max_length=64, default='')
|
||||
src_ip = models.CharField(max_length=128, default='')
|
||||
|
||||
cluster_node = models.CharField(
|
||||
max_length=128, default=None, blank=True, null=True, db_index=True
|
||||
)
|
||||
|
||||
# "fake" declarations for type checking
|
||||
objects: 'models.manager.Manager[UserService]'
|
||||
objects: 'models.manager.Manager["UserService"]'
|
||||
properties: 'models.manager.RelatedManager[UserServiceProperty]'
|
||||
sessions: 'models.manager.RelatedManager[UserServiceSession]'
|
||||
accounting: 'AccountUsage'
|
||||
|
||||
class Meta(UUIDModel.Meta):
|
||||
@ -453,6 +450,30 @@ class UserService(UUIDModel): # pylint: disable=too-many-public-methods
|
||||
|
||||
self.deployed_service.account.stopUsageAccounting(self)
|
||||
|
||||
def initSession(self) -> str:
|
||||
"""
|
||||
Starts a new session for this user deployed service.
|
||||
Returns the session id
|
||||
"""
|
||||
session = self.sessions.create()
|
||||
return session.session_id
|
||||
|
||||
def closeSession(self, sessionId: str) -> None:
|
||||
if sessionId == '':
|
||||
# Close all sessions
|
||||
for session in self.sessions.all():
|
||||
session.close()
|
||||
else:
|
||||
# Close a specific session
|
||||
try:
|
||||
session = self.sessions.get(session_id=sessionId)
|
||||
session.close()
|
||||
except Exception: # Does not exists, log it and ignore it
|
||||
logger.warning(
|
||||
'Session %s does not exists for user deployed service %s'
|
||||
% (sessionId, self.id)
|
||||
)
|
||||
|
||||
def isUsable(self) -> bool:
|
||||
"""
|
||||
Returns if this service is usable
|
||||
@ -599,8 +620,13 @@ class UserService(UUIDModel): # pylint: disable=too-many-public-methods
|
||||
|
||||
:note: If destroy raises an exception, the deletion is not taken.
|
||||
"""
|
||||
toDelete = kwargs['instance']
|
||||
toDelete: 'UserService' = kwargs['instance']
|
||||
# Clear environment
|
||||
toDelete.getEnvironment().clearRelatedData()
|
||||
# Ensure all sessions are closed (invoke with '' to close all sessions)
|
||||
# In fact, sessions are going to be deleted also, but we give then
|
||||
# the oportunity to execute some code before deleting them
|
||||
toDelete.closeSession('')
|
||||
|
||||
# Clear related logs to this user service
|
||||
log.clearLogs(toDelete)
|
||||
|
91
server/src/uds/models/user_service_session.py
Normal file
91
server/src/uds/models/user_service_session.py
Normal file
@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
from uds.core.managers import cryptoManager
|
||||
from .user_service import UserService
|
||||
from .util import getSqlDatetime
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _session_id_generator() -> str:
|
||||
"""
|
||||
Generates a new session id
|
||||
"""
|
||||
return cryptoManager().unique()
|
||||
|
||||
|
||||
class UserServiceSession(models.Model): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
Properties for User Service.
|
||||
The value field is a Text field, so we can put whatever we want in it
|
||||
"""
|
||||
|
||||
session_id = models.CharField(max_length=128, db_index=True, default=_session_id_generator, blank=True)
|
||||
start = models.DateTimeField(default=getSqlDatetime)
|
||||
end = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
user_service = models.ForeignKey(
|
||||
UserService, on_delete=models.CASCADE, related_name='sessions'
|
||||
)
|
||||
|
||||
# "fake" declarations for type checking
|
||||
objects: 'models.manager.Manager["UserServiceSession"]'
|
||||
|
||||
class Meta:
|
||||
"""
|
||||
Meta class to declare default order and unique multiple field index
|
||||
"""
|
||||
|
||||
db_table = 'uds__user_service_session'
|
||||
app_label = 'uds'
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=['session_id', 'user_service'], name='u_session_userservice'
|
||||
)
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'Session {}. ({} to {}'.format(
|
||||
self.session_id, self.start, self.end
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Ends the session
|
||||
"""
|
||||
self.end = getSqlDatetime()
|
||||
self.save(update_fields=['end'])
|
Loading…
Reference in New Issue
Block a user