1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-03-11 00:58:39 +03:00

Replacing several diferent "registration" tables to a single, versatile one.

This commit is contained in:
Adolfo Gómez García 2023-07-12 19:35:48 +02:00
parent 81db3278bd
commit 012f8de6c5
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
11 changed files with 157 additions and 136 deletions

View File

@ -71,4 +71,4 @@ class ActorRegisterTest(rest.test.RESTActorTestCase):
token = response.json()['result']
# Ensure database contains the registered token
self.assertEqual(models.ActorToken.objects.filter(token=token).count(), 1)
self.assertEqual(models.RegisteredServers.objects.filter(token=token).count(), 1)

View File

@ -35,7 +35,7 @@ import typing
from django.utils.translation import gettext_lazy as _
from uds.models import ActorToken
from uds.models import RegisteredServers
from uds.REST.exceptions import RequestError, NotFound
from uds.REST.model import ModelHandler, OK
from uds.core.util import permissions
@ -47,7 +47,8 @@ logger = logging.getLogger(__name__)
class ActorTokens(ModelHandler):
model = ActorToken
model = RegisteredServers
model_filter = {'kind': RegisteredServers.ServerType.ACTOR}
table_title = _('Actor tokens')
table_fields = [
@ -62,21 +63,22 @@ class ActorTokens(ModelHandler):
{'log_level': {'title': _('Log level')}},
]
def item_as_dict(self, item: ActorToken) -> typing.Dict[str, typing.Any]:
def item_as_dict(self, item: RegisteredServers) -> typing.Dict[str, typing.Any]:
data = item.data or {}
return {
'id': item.token,
'name': str(_('Token isued by {} from {}')).format(
item.username, item.hostname or item.ip
),
'name': str(_('Token isued by {} from {}')).format(item.username, item.hostname or item.ip),
'stamp': item.stamp,
'username': item.username,
'ip': item.ip,
'host': f'{item.ip} - {item.mac}',
'host': f'{item.ip} - {data.get("mac")}',
'hostname': item.hostname,
'pre_command': item.pre_command,
'post_command': item.post_command,
'runonce_command': item.runonce_command,
'log_level': LogLevel.fromActorLevel(item.log_level).name # ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
'pre_command': data.get('pre_command', ''),
'post_command': data.get('post_command', ''),
'runonce_command': data.get('runonce_command', ''),
'log_level': LogLevel.fromActorLevel(
data.get('log_level', 2)
).name, # ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
}
def delete(self) -> str:

View File

@ -36,10 +36,10 @@ import functools
import enum
from uds.models import (
ActorToken,
UserService,
Service,
TicketStore,
RegisteredServers,
)
from uds.core.util.model import getSqlDatetimeAsUnix, getSqlDatetime
@ -240,7 +240,9 @@ 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
RegisteredServers.objects.get(
token=self._params['token'], kind=RegisteredServers.ServerType.ACTOR
) # Not assigned, because only needs check
clearFailedIp(self._request)
except Exception:
# Increase failed attempts
@ -273,44 +275,55 @@ class Register(ActorV3Action):
name = 'register'
def post(self) -> typing.MutableMapping[str, typing.Any]:
actorToken: ActorToken
try:
# If already exists a token for this MAC, return it instead of creating a new one, and update the information...
actorToken = ActorToken.objects.get(mac=self._params['mac'])
# Update parameters
actorToken.username = self._user.pretty_name
actorToken.ip_from = self._request.ip
actorToken.ip = self._params['ip']
actorToken.hostname = self._params['hostname']
actorToken.pre_command = self._params['pre_command']
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: RegisteredServers
# If already exists a token for this MAC, return it instead of creating a new one, and update the information...
# Look for a token for this mac. mac is "inside" data, so we must filter first by type and then ensure mac is inside data
# and mac is the requested one
found = False
for actorToken in RegisteredServers.objects.filter(kind=RegisteredServers.ServerType.ACTOR):
if actorToken.data and actorToken.data.get('mac', '') == self._params['mac']:
# Update parameters
actorToken.username = self._user.pretty_name
actorToken.ip_from = self._request.ip
actorToken.ip = self._params['ip']
actorToken.hostname = self._params['hostname']
actorToken.data = { # type: ignore
'mac': self._params['mac'],
'pre_command': self._params['pre_command'],
'post_command': self._params['post_command'],
'run_once_command': self._params['run_once_command'],
'log_level': self._params['log_level'],
'custom': self._params.get('custom', ''),
}
actorToken.stamp = getSqlDatetime()
actorToken.save()
logger.info('Registered actor %s', self._params)
found = True
break
if not found:
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'],
'data': { # type: ignore
'mac': self._params['mac'],
'pre_command': self._params['pre_command'],
'post_command': self._params['post_command'],
'run_once_command': self._params['run_once_command'],
'log_level': self._params['log_level'],
'custom': self._params.get('custom', ''),
},
'token': secrets.token_urlsafe(36),
'kind': RegisteredServers.ServerType.ACTOR,
'stamp': getSqlDatetime(),
}
if 'custom' in self._params:
kwargs['custom'] = self._params['custom']
actorToken = ActorToken.objects.create(**kwargs)
return ActorV3Action.actorResult(actorToken.token)
actorToken = RegisteredServers.objects.create(**kwargs)
return ActorV3Action.actorResult(actorToken.token) # type: ignore # actorToken is always assigned
class Initialize(ActorV3Action):
@ -396,7 +409,9 @@ class Initialize(ActorV3Action):
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
RegisteredServers.objects.get(
token=token, kind=RegisteredServers.ServerType.ACTOR
) # 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()
@ -430,7 +445,7 @@ class Initialize(ActorV3Action):
service.aliases.create(alias=alias_token)
return initialization_result(userService.uuid, userService.unique_id, osData, alias_token)
except (ActorToken.DoesNotExist, Service.DoesNotExist):
except (RegisteredServers.DoesNotExist, Service.DoesNotExist):
raise BlockAccess() from None
@ -706,8 +721,8 @@ class Ticket(ActorV3Action):
try:
# Simple check that token exists
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
except ActorToken.DoesNotExist:
RegisteredServers.objects.get(token=self._params['token'], kind=RegisteredServers.ServerType.ACTOR) # Not assigned, because only needs check
except RegisteredServers.DoesNotExist:
raise BlockAccess() from None # If too many blocks...
try:

View File

@ -74,6 +74,7 @@ class ServerRegister(Handler):
token=secrets.token_urlsafe(36),
stamp=getSqlDatetime(),
kind=self._params['type'],
data=self._params.get('data', None),
)
except Exception as e:
return {'result': '', 'stamp': now, 'error': str(e)}
@ -82,6 +83,7 @@ class ServerRegister(Handler):
class ServersTokens(ModelHandler):
model = models.RegisteredServers
model_filter = {'kind__in': [models.RegisteredServers.ServerType.TUNNEL, models.RegisteredServers.ServerType.OTHER]}
path = 'servers'
name = 'tokens'

View File

@ -709,6 +709,8 @@ class ModelHandler(BaseModelHandler):
# Which model does this manage, must be a django model ofc
model: 'typing.ClassVar[typing.Type[models.Model]]'
# If the model is filtered (for overviews)
model_filter: 'typing.ClassVar[typing.Optional[typing.Mapping[str, typing.Any]]]' = None
# By default, filter is empty
fltr: typing.Optional[str] = None
@ -935,6 +937,9 @@ class ModelHandler(BaseModelHandler):
*prefetch
)
if self.model_filter is not None:
query = query.filter(**self.model_filter)
for item in query:
try:
if (

View File

@ -61,8 +61,7 @@ class ObjectType(enum.Enum):
# PROXY_TYPE = (15, models.Proxy) has been removed
METAPOOL = (16, models.MetaPool)
ACCOUNT = (17, models.Account)
ACTOR_TOKEN = (18, models.ActorToken)
TUNNEL_TOKEN = (19, models.RegisteredServers)
REGISTERED_SERVER = (19, models.RegisteredServers)
ACCOUNT_USAGE = (20, models.AccountUsage)
IMAGE = (21, models.Image)
LOG = (22, models.Log)

View File

@ -356,24 +356,14 @@ class Command(BaseCommand):
tree[counter('GALLERY')] = gallery
# Actor tokens
actorTokens: typing.Dict[str, typing.Any] = {}
for actorToken in models.ActorToken.objects.all():
actorTokens[actorToken.hostname] = getSerializedFromModel(
actorToken, passwordFields=['token']
# Rest of registerd servers
registeredServers: typing.Dict[str, typing.Any] = {}
for i, registeredServer in enumerate(models.RegisteredServers.objects.all()):
registeredServers[f'{i}'] = getSerializedFromModel(
registeredServer
)
tree[counter('ACTORTOKENS')] = actorTokens
# Tunnel tokens
tunnelTokens: typing.Dict[str, typing.Any] = {}
for tunnelToken in models.RegisteredServers.objects.all():
tunnelTokens[tunnelToken.hostname] = getSerializedFromModel(
tunnelToken, passwordFields=['token']
)
tree[counter('TUNNELTOKENS')] = tunnelTokens
self.stdout.write(yaml.safe_dump(tree, default_flow_style=False))
except Exception as e:

View File

@ -1,7 +1,58 @@
# Generated by Django 4.2.1 on 2023-07-12 04:04
import typing
from django.db import migrations, models
ACTOR_TYPE = 2 # Hardcoded value from uds/models/registered_servers.py
def migrate_old_actor_tokens(apps, schema_editor):
try:
RegisteredServers = apps.get_model('uds', 'RegisteredServers')
ActorToken = apps.get_model('uds', 'ActorToken')
for token in ActorToken.objects.all():
RegisteredServers.objects.create(
username=token.username,
ip_from=token.ip_from,
ip=token.ip,
ip_version=token.ip_version,
hostname=token.hostname,
token=token.token,
stamp=token.stamp,
kind=ACTOR_TYPE,
data={
'mac': token.mac,
'pre_command': token.pre_command,
'post_command': token.post_command,
'runonce_command': token.runonce_command,
'log_level': token.log_level,
'custom': token.custom,
}
)
except Exception as e:
if 'no such table' not in str(e):
# Pytest is running this method twice??
raise e
def revert_migration_to_old_actor_tokens(apps, schema_editor):
RegisteredServers = apps.get_model('uds', 'RegisteredServers')
ActorToken = apps.get_model('uds', 'ActorToken')
for server in RegisteredServers.objects.filter(kind=ACTOR_TYPE):
ActorToken.objects.create(
username=server.username,
ip_from=server.ip_from,
ip=server.ip,
ip_version=server.ip_version,
hostname=server.hostname,
token=server.token,
stamp=server.stamp,
mac=server.data['mac'],
pre_command=server.data['pre_command'],
post_command=server.data['post_command'],
runonce_command=server.data['runonce_command'],
log_level=server.data['log_level'],
custom=server.data['custom'],
)
# Delete the server
server.delete()
class Migration(migrations.Migration):
dependencies = [
@ -13,9 +64,31 @@ class Migration(migrations.Migration):
'TunnelToken',
'RegisteredServers',
),
migrations.RemoveConstraint(
model_name="registeredservers",
name="tt_ip_hostname",
),
migrations.AddField(
model_name="registeredservers",
name="kind",
field=models.IntegerField(default=1),
),
migrations.AddField(
model_name="registeredservers",
name="ip_version",
field=models.IntegerField(default=4),
),
migrations.AddField(
model_name="registeredservers",
name="data",
field=models.JSONField(blank=True, default=None, null=True),
),
migrations.RunPython(
migrate_old_actor_tokens,
revert_migration_to_old_actor_tokens,
atomic=True,
),
migrations.DeleteModel(
name="ActorToken",
),
]

View File

@ -103,8 +103,7 @@ from .account_usage import AccountUsage
from .tag import Tag, TaggingMixin
# Tokens
from .actor_token import ActorToken
from .tunnel_token import RegisteredServers
from .registered_servers import RegisteredServers
# Notifications & Alerts
from .notifications import Notification, Notifier, LogLevel

View File

@ -1,63 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2023 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.U. 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.
'''
Author: Adolfo Gómez, dkmaster at dkmon dot com
'''
from django.db import models
from .consts import MAX_IPV6_LENGTH, MAX_DNS_NAME_LENGTH
class ActorToken(models.Model):
"""
UDS Actors tokens on DB
"""
username = models.CharField(max_length=128)
ip_from = models.CharField(max_length=MAX_IPV6_LENGTH)
ip = models.CharField(max_length=MAX_IPV6_LENGTH)
ip_version = models.IntegerField(default=4) # Version of ip fields
hostname = models.CharField(max_length=MAX_DNS_NAME_LENGTH)
mac = models.CharField(max_length=128, db_index=True, unique=True)
pre_command = models.CharField(max_length=255, blank=True, default='')
post_command = models.CharField(max_length=255, blank=True, default='')
runonce_command = models.CharField(max_length=255, blank=True, default='')
log_level = models.IntegerField()
token = models.CharField(max_length=48, db_index=True, unique=True)
stamp = models.DateTimeField() # Date creation or validation of this entry
# New fields for 4.0, optional "custom" data, to be interpreted by specific code
custom = models.TextField(blank=True, default='')
class Meta: # pylint: disable=too-few-public-methods
app_label = 'uds'
def __str__(self):
return f'<ActorToken {self.token} created on {self.stamp} by {self.username} from {self.hostname}/{self.ip_from}>'

View File

@ -55,7 +55,8 @@ class RegisteredServers(models.Model):
"""
class ServerType(enum.IntFlag):
TUNNEL = 1
OTHER = 2
ACTOR = 2
OTHER = 99
def as_str(self) -> str:
return self.name.lower() # type: ignore
@ -63,6 +64,8 @@ class RegisteredServers(models.Model):
username = models.CharField(max_length=128)
ip_from = models.CharField(max_length=MAX_IPV6_LENGTH)
ip = models.CharField(max_length=MAX_IPV6_LENGTH)
ip_version = models.IntegerField(default=4) # 4 or 6, version of ip fields
hostname = models.CharField(max_length=MAX_DNS_NAME_LENGTH)
token = models.CharField(max_length=48, db_index=True, unique=True)
@ -70,14 +73,10 @@ class RegisteredServers(models.Model):
kind = models.IntegerField(default=ServerType.TUNNEL.value) # Defaults to tunnel server, so we can migrate from previous versions
# "fake" declarations for type checking
# objects: 'models.manager.Manager[TunnelToken]'
data = models.JSONField(null=True, blank=True, default=None)
class Meta: # pylint: disable=too-few-public-methods
app_label = 'uds'
constraints = [
models.UniqueConstraint(fields=['ip', 'hostname'], name='tt_ip_hostname')
]
@staticmethod
def validateToken(