1
0
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:
Adolfo Gómez García 2022-08-07 13:24:33 +02:00
parent e5c8b9c763
commit 78a3216b51
14 changed files with 562 additions and 194 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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]:

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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"
),
),
]

View File

@ -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

View File

@ -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)

View 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'])