1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-23 17:34:17 +03:00

added initial export command for relevant UDS entities

This commit is contained in:
Adolfo Gómez García 2022-10-26 18:32:52 +02:00
parent f3dd5753a3
commit ad269b3c28
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
8 changed files with 546 additions and 16 deletions

View File

@ -104,7 +104,7 @@ class Users(DetailHandler):
'is_admin', 'is_admin',
'last_access', 'last_access',
'parent', 'parent',
'mfaData', 'mfa_data',
)) ))
) )
) )
@ -128,7 +128,7 @@ class Users(DetailHandler):
'is_admin', 'is_admin',
'last_access', 'last_access',
'parent', 'parent',
'mfaData', 'mfa_data',
), ),
) )
res['id'] = u.uuid res['id'] = u.uuid
@ -207,9 +207,9 @@ class Users(DetailHandler):
valid_fields.append('password') valid_fields.append('password')
self._params['password'] = cryptoManager().hash(self._params['password']) self._params['password'] = cryptoManager().hash(self._params['password'])
if 'mfaData' in self._params: if 'mfa_data' in self._params:
valid_fields.append('mfaData') valid_fields.append('mfa_data')
self._params['mfaData'] = self._params['mfaData'].strip() self._params['mfa_data'] = self._params['mfa_data'].strip()
fields = self.readFieldsFromParams(valid_fields) fields = self.readFieldsFromParams(valid_fields)
if not self._user.is_admin: if not self._user.is_admin:

View File

@ -107,7 +107,7 @@ class InternalDBAuth(auths.Authenticator):
def mfaIdentifier(self, username: str) -> str: def mfaIdentifier(self, username: str) -> str:
try: try:
return self.dbAuthenticator().users.get(name=username, state=State.ACTIVE).mfaData return self.dbAuthenticator().users.get(name=username, state=State.ACTIVE).mfa_data
finally: finally:
return '' return ''

View File

@ -136,7 +136,7 @@ class RadiusAuth(auths.Authenticator):
) )
def mfaStorageKey(self, username: str) -> str: def mfaStorageKey(self, username: str) -> str:
return 'mfa_' + self.dbAuthenticator().uuid + username return 'mfa_' + str(self.dbAuthenticator().uuid) + username
def mfaIdentifier(self, username: str) -> str: def mfaIdentifier(self, username: str) -> str:
return self.storage.getPickle(self.mfaStorageKey(username)) or '' return self.storage.getPickle(self.mfaStorageKey(username)) or ''

View File

@ -0,0 +1,531 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 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.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
"""
import logging
import typing
import csv
import yaml
from django.core.management.base import BaseCommand
from uds import models
if typing.TYPE_CHECKING:
import argparse
from django.db.models import Model
from uds.models.uuid_model import UUIDModel
logger = logging.getLogger(__name__)
ModelType = typing.TypeVar('ModelType', bound='UUIDModel')
T = typing.TypeVar('T')
def uuid_object_exporter(obj: 'UUIDModel') -> typing.Dict[str, typing.Any]:
"""
Exports a uuid model to a dict
"""
return {
'uuid': obj.uuid,
}
def managed_object_exporter(
obj: models.ManagedObjectModel,
) -> typing.Dict[str, typing.Any]:
"""
Exports a managed object to a dict
"""
# Get uuid model
m = uuid_object_exporter(obj)
# Extend with managed object fields
m.update(
{
'name': obj.name,
'comments': obj.comments,
'data': obj.data,
'data_type': obj.data_type,
}
)
return m
def provider_exporter(provider: models.Provider) -> typing.Dict[str, typing.Any]:
"""
Exports a provider to a dict
"""
p = managed_object_exporter(provider)
p['maintenance_mode'] = provider.maintenance_mode
return p
def service_exporter(service: models.Service) -> typing.Dict[str, typing.Any]:
"""
Exports a service to a dict
"""
s = managed_object_exporter(service)
s['provider'] = service.provider.uuid
s['token'] = service.token
return s
def authenticator_exporter(
authenticator: models.Authenticator,
) -> typing.Dict[str, typing.Any]:
"""
Exports an authenticator to a dict
"""
a = managed_object_exporter(authenticator)
a['priority'] = authenticator.priority
a['provider'] = authenticator.small_name
a['visible'] = authenticator.visible
return a
def user_exporter(user: models.User) -> typing.Dict[str, typing.Any]:
"""
Exports a user to a dict
"""
u = uuid_object_exporter(user)
u.update(
{
'manager': user.manager.uuid,
'name': user.name,
'comments': user.comments,
'real_name': user.real_name,
'state': user.state,
'password': user.password,
'mfa_data': user.mfa_data,
'staff_member': user.staff_member,
'is_admin': user.is_admin,
'last_access': user.last_access,
'parent': user.parent,
'created': user.created,
'groups': [g.uuid for g in user.groups.all()],
}
)
return u
def group_export(group: models.Group) -> typing.Dict[str, typing.Any]:
"""
Exports a group to a dict
"""
g = uuid_object_exporter(group)
g.update(
{
'manager': group.manager.uuid,
'name': group.name,
'comments': group.comments,
'state': group.state,
'is_meta': group.is_meta,
'meta_if_any': group.meta_if_any,
'created': group.created,
}
)
return g
def transport_exporter(transport: models.Transport) -> typing.Dict[str, typing.Any]:
"""
Exports a transport to a dict
"""
t = managed_object_exporter(transport)
t.update(
{
'priority': transport.priority,
'nets_positive': transport.nets_positive,
'allowed_oss': transport.allowed_oss,
'label': transport.label,
'networks': [n.uuid for n in transport.networks.all()],
}
)
return t
def network_exporter(network: models.Network) -> typing.Dict[str, typing.Any]:
"""
Exports a network to a dict
"""
n = uuid_object_exporter(network)
n.update(
{
'name': network.name,
'net_start': network.net_start,
'net_end': network.net_end,
'net_string': network.net_string,
}
)
return n
def osmanager_exporter(osmanager: models.OSManager) -> typing.Dict[str, typing.Any]:
"""
Exports an osmanager to a dict
"""
o = managed_object_exporter(osmanager)
return o
class Command(BaseCommand):
help = 'Export entities from UDS to be imported in another UDS instance'
VALID_ENTITIES: typing.Mapping[str, typing.Callable[[], str]]
verbose: bool = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.VALID_ENTITIES = {
'providers': self.export_providers,
'services': self.export_services,
'authenticators': self.export_authenticators,
'users': self.export_users,
'groups': self.export_groups,
'networks': self.export_networks,
'transports': self.export_transports,
'osmanagers': self.export_osmanagers,
}
def add_arguments(self, parser: 'argparse.ArgumentParser') -> None:
# Accepts a list of valid entities to export
parser.add_argument(
'entities',
nargs='+',
choices=self.VALID_ENTITIES.keys(),
default=self.VALID_ENTITIES.keys(),
help='Entities to export',
)
# Output file name (will be appended .csv or .yaml)
parser.add_argument(
'--output',
action='store',
dest='output',
default='/tmp/export.yaml',
help='Output file name. Defaults to /tmp/export.yaml',
)
# Filter ALL entities by name
parser.add_argument(
'--filter-name',
action='store',
dest='filter_name',
default=None,
help='Filter ALL entities by name',
)
# filter ALL entities by uuid
parser.add_argument(
'--filter-uuid',
action='store',
dest='filter_uuid',
default=None,
help='Filter ALL entities by uuid',
)
# quiet mode
parser.add_argument(
'--quiet',
action='store_false',
dest='verbose',
default=True,
help='Quiet mode',
)
def handle(self, *args, **options) -> None:
self.verbose = options['verbose']
if self.verbose:
self.stderr.write(f'Exporting entities: {",".join(options["entities"])}')
# Compose filter name for kwargs
filter_kwargs = {}
if options['filter_name']:
filter_kwargs['name__icontains'] = options['filter_name']
if options['filter_uuid']:
filter_kwargs['uuid__icontains'] = options['filter_uuid']
# some entities are redundant, so remove them from the list
entities = self.remove_reduntant_entities(options['entities'])
# For each entity, export it as yaml to output file
with open(options['output'], 'w') as f:
for entity in entities:
self.stderr.write(f'Exporting {entity}')
f.write(self.VALID_ENTITIES[entity](**filter_kwargs))
f.write('')
if self.verbose:
self.stderr.write(f'Exported to {options["output"]}')
def apply_filter(
self, model: typing.Type[ModelType], **kwargs: str
) -> typing.Iterable[ModelType]:
"""
Applies a filter to a model
"""
if self.verbose:
# Explit xxx__icontains=yyy to xxx=yyy
values = [f'{k.split("__")[0]}={v}' for k, v in kwargs.items()]
self.stderr.write(f'Filtering {model.__name__} by {",".join(values)}')
yield from model.objects.all().filter(**kwargs)
def output_count(
self, message: str, iterable: typing.Iterable[T]
) -> typing.Iterable[T]:
"""
Outputs the count of an iterable
"""
count = 0
for v in iterable:
count += 1
if self.verbose:
self.stderr.write(f'{message} {count}', ending='\r')
yield v
if self.verbose:
self.stderr.write('\n') # New line after count
def export_providers(self, **kwargs: str) -> str:
"""
Exports all providers to a list of dicts
"""
return '# Providers\n' + yaml.safe_dump(
[provider_exporter(p) for p in self.apply_filter(models.Provider, **kwargs)]
)
def export_services(self, **kwargs: str) -> str:
# First, locate providers for services with the filter
services_list = list(
self.output_count(
'Filtering services', self.apply_filter(models.Service, **kwargs)
)
)
providers_list = set(
[
s.provider
for s in self.output_count('Filtering providers', services_list)
]
)
# Now, export those providers
providers = [
provider_exporter(p)
for p in self.output_count('Saving providers', providers_list)
]
# Then, export services with the filter
services = [
service_exporter(s)
for s in self.output_count('Saving services', services_list)
]
return (
'# Providers\n'
+ yaml.safe_dump(providers)
+ '# Services\n'
+ yaml.safe_dump(services)
)
def export_authenticators(self, **kwargs: str) -> str:
"""
Exports all authenticators to a list of dicts
"""
return '# Authenticators\n' + yaml.safe_dump(
[
authenticator_exporter(a)
for a in self.output_count(
'Saving authenticators',
self.apply_filter(models.Authenticator, **kwargs),
)
]
)
def export_users(self, **kwargs: str) -> str:
"""
Exports all users to a list of dicts
"""
# first, locate authenticators for users with the filter
users_list = list(
self.output_count(
'Filtering users', self.apply_filter(models.User, **kwargs)
)
)
authenticators_list = set(
[
u.manager
for u in self.output_count('Filtering authenticators', users_list)
]
)
# Now, groups that contains those users
groups_list = set()
for u in self.output_count('Filtering groups', users_list):
groups_list.update(u.groups.all())
# now, export those authenticators
authenticators = [
authenticator_exporter(a)
for a in self.output_count('Saving authenticators', authenticators_list)
]
# then, export those groups
groups = [
group_export(g) for g in self.output_count('Saving groups', groups_list)
]
# finally, export users with the filter
users = [
user_exporter(u) for u in self.output_count('Saving users', users_list)
]
return (
'# Authenticators\n'
+ yaml.safe_dump(authenticators)
+ '# Groups\n'
+ yaml.safe_dump(groups)
+ '# Users\n'
+ yaml.safe_dump(users)
)
def export_groups(self, **kwargs: str) -> str:
"""
Exports all groups to a list of dicts
"""
# First export authenticators for groups with the filter
groups_list = list(
self.output_count(
'Filtering groups', self.apply_filter(models.Group, **kwargs)
)
)
authenticators_list = set(
[
g.manager
for g in self.output_count('Filtering authenticators', groups_list)
]
)
authenticators = [
authenticator_exporter(a)
for a in self.output_count('Saving authenticators', authenticators_list)
]
# then, export groups with the filter
groups = [
group_export(g) for g in self.output_count('Saving groups', groups_list)
]
return (
'# Authenticators\n'
+ yaml.safe_dump(authenticators)
+ '# Groups\n'
+ yaml.safe_dump(groups)
)
def export_networks(self, **kwargs: str) -> str:
"""
Exports all networks to a list of dicts
"""
return '# Networks\n' + yaml.safe_dump(
[
network_exporter(n)
for n in self.output_count(
'Saving networks', self.apply_filter(models.Network, **kwargs)
)
]
)
def export_transports(self, **kwargs: str) -> str:
"""
Exports all transports to a list of dicts
"""
# First, export networks for transports with the filter
transports_list = list(
self.output_count(
'Filtering transports', self.apply_filter(models.Transport, **kwargs)
)
)
networks_list = set()
for t in self.output_count('Filtering networks', transports_list):
networks_list.update(t.networks.all())
networks = [
network_exporter(n)
for n in self.output_count('Saving networks', networks_list)
]
# then, export transports with the filter
transports = [
transport_exporter(t)
for t in self.output_count('Saving transports', transports_list)
]
return (
'# Networks\n'
+ yaml.safe_dump(networks)
+ '# Transports\n'
+ yaml.safe_dump(transports)
)
def export_osmanagers(self, **kwargs: str) -> str:
"""
Exports all osmanagers to a list of dicts
"""
return '# OSManagers\n' + yaml.safe_dump(
[
osmanager_exporter(o)
for o in self.output_count(
'Saving osmanagers', self.apply_filter(models.OSManager, **kwargs)
)
]
)
def remove_reduntant_entities(self, entities: typing.List[str]) -> typing.List[str]:
"""
Removes redundant entities from the list
"""
REPLACES: typing.Mapping[str, typing.List[str]] = {
'users': ['authenticators', 'groups'],
'groups': ['authenticators'],
'authenticators': [],
'transports': ['networks'],
'networks': [],
'osmanagers': [],
'services': ['providers'],
'providers': [],
}
entities = list(set(entities)) # remove duplicates
# Remove entities that are replaced by other entities
for entity in entities:
for replace in REPLACES.get(entity, []):
if replace in entities:
entities.remove(replace)
return entities

View File

@ -56,7 +56,7 @@ class Group(UUIDModel):
This class represents a group, associated with one authenticator This class represents a group, associated with one authenticator
""" """
manager: 'models.ForeignKey[Group, Authenticator]' = UnsavedForeignKey( manager: 'models.ForeignKey[Authenticator]' = UnsavedForeignKey(
Authenticator, on_delete=models.CASCADE, related_name='groups' Authenticator, on_delete=models.CASCADE, related_name='groups'
) )
name = models.CharField(max_length=128, db_index=True) name = models.CharField(max_length=128, db_index=True)

View File

@ -68,7 +68,7 @@ class ManagedObjectModel(UUIDModel):
""" """
Returns an environment valid for the record this object represents Returns an environment valid for the record this object represents
""" """
return Environment.getEnvForTableElement(self._meta.verbose_name, self.id) return Environment.getEnvForTableElement(self._meta.verbose_name, self.id) # type: ignore
def deserialize(self, obj: Module, values: typing.Optional[typing.Dict[str, str]]): def deserialize(self, obj: Module, values: typing.Optional[typing.Dict[str, str]]):
""" """

View File

@ -61,12 +61,12 @@ class Service(ManagedObjectModel, TaggingMixin): # type: ignore
Server configuration). Server configuration).
""" """
provider: 'models.ForeignKey[Service, Provider]' = models.ForeignKey( provider: 'models.ForeignKey[Provider]' = models.ForeignKey(
Provider, related_name='services', on_delete=models.CASCADE Provider, related_name='services', on_delete=models.CASCADE
) )
# Proxy for this service # Proxy for this service
proxy: 'models.ForeignKey[Service, Proxy]' = models.ForeignKey( proxy: 'models.ForeignKey[Proxy|None]' = models.ForeignKey(
Proxy, null=True, blank=True, related_name='services', on_delete=models.CASCADE Proxy, null=True, blank=True, related_name='services', on_delete=models.CASCADE
) )

View File

@ -69,7 +69,7 @@ class User(UUIDModel):
password = models.CharField( password = models.CharField(
max_length=128, default='' max_length=128, default=''
) # Only used on "internal" sources or sources that "needs password" ) # Only used on "internal" sources or sources that "needs password"
mfaData = models.CharField( mfa_data = models.CharField(
max_length=128, default='' max_length=128, default=''
) # Only used on "internal" sources ) # Only used on "internal" sources
staff_member = models.BooleanField( staff_member = models.BooleanField(
@ -84,7 +84,6 @@ class User(UUIDModel):
objects: 'models.BaseManager[User]' objects: 'models.BaseManager[User]'
groups: 'models.manager.RelatedManager[Group]' groups: 'models.manager.RelatedManager[Group]'
userServices: 'models.manager.RelatedManager[UserService]' userServices: 'models.manager.RelatedManager[UserService]'
mfa: 'models.manager.RelatedManager[MFA]'
class Meta(UUIDModel.Meta): class Meta(UUIDModel.Meta):
""" """
@ -224,8 +223,8 @@ class User(UUIDModel):
If the key exists, the custom data will always contain something, but may be the values are the default ones. If the key exists, the custom data will always contain something, but may be the values are the default ones.
""" """
with storage.StorageAccess('manager' + self.manager.uuid) as store: with storage.StorageAccess('manager' + str(self.manager.uuid)) as store:
return store[self.uuid + '_' + key] return store[str(self.uuid) + '_' + key]
def __str__(self): def __str__(self):
return 'User {} (id:{}) from auth {}'.format( return 'User {} (id:{}) from auth {}'.format(
@ -248,7 +247,7 @@ class User(UUIDModel):
# be removed # be removed
toDelete.getManager().removeUser(toDelete.name) toDelete.getManager().removeUser(toDelete.name)
# Remove related stored values # Remove related stored values
with storage.StorageAccess('manager' + toDelete.manager.uuid) as store: with storage.StorageAccess('manager' + str(toDelete.manager.uuid)) as store:
for key in store.keys(): for key in store.keys():
store.delete(key) store.delete(key)