fixes for python 3.x

This commit is contained in:
Adolfo Gómez García 2019-06-11 05:33:25 +02:00
parent 0b97c15ca8
commit 6309047a34
20 changed files with 254 additions and 283 deletions

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,12 +30,9 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
__updated__ = '2014-11-12'
class AuthsFactory(object):
class AuthsFactory:
"""
This class holds the register of all known authentication modules
inside UDS.

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -31,20 +31,18 @@ Base module for all authenticators
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
from uds.core import Module
from django.utils.translation import ugettext_noop as _
from uds.core.auths.GroupsManager import GroupsManager
from uds.core.auths.Exceptions import InvalidUserException
import logging
__updated__ = '2019-01-21'
from django.utils.translation import ugettext_noop as _
from uds.core import Module
from uds.core.auths.GroupsManager import GroupsManager
from uds.core.auths.Exceptions import InvalidUserException
logger = logging.getLogger(__name__)
class Authenticator(Module):
class Authenticator(Module): # pylint: disable=too-many-public-methods
"""
This class represents the base interface to implement authenticators.
@ -181,7 +179,6 @@ class Authenticator(Module):
Default implementation does nothing
"""
pass
def dbAuthenticator(self):
"""
@ -430,7 +427,6 @@ class Authenticator(Module):
calling its :py:meth:`uds.core.auths.GroupsManager.validate` method with groups names provided by the authenticator itself
(for example, LDAP, AD, ...)
"""
pass
def getJavascript(self, request):
"""
@ -582,7 +578,6 @@ class Authenticator(Module):
:note: By default, this will do nothing, as we can only modify "accesory" internal
data of users.
"""
pass
def createGroup(self, groupData):
"""
@ -609,7 +604,6 @@ class Authenticator(Module):
Take care with whatever you modify here, you can even modify provided
name (group name) to a new one!
"""
pass
def modifyGroup(self, groupData):
"""
@ -635,7 +629,6 @@ class Authenticator(Module):
Note: 'name' output parameter will be ignored
"""
pass
def removeUser(self, username):
"""
@ -650,7 +643,6 @@ class Authenticator(Module):
If this method raises an exception, the user will not be removed from UDS
"""
pass
# We don't have a "modify" group option. Once u have created it, the only way of changing it if removing it an recreating it with another name
@ -667,4 +659,3 @@ class Authenticator(Module):
If this method raises an exception, the group will not be removed from UDS
"""
pass

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,30 +30,23 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
__updated__ = '2014-02-19'
class AuthenticatorException(Exception):
"""
Generic authentication exception
"""
pass
class InvalidUserException(AuthenticatorException):
"""
Invalid user specified. The user cant access the requested service
"""
pass
class InvalidAuthenticatorException(AuthenticatorException):
"""
Invalida authenticator has been specified
"""
pass
class Redirect(Exception):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@ -31,37 +31,40 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
import six
import logging
import typing
from uds.models import Group as DBGroup
# Imports for type checking
if typing.TYPE_CHECKING:
from uds.core.auths.BaseAuthenticator import Authenticator as AuthenticatorInstance
__updated__ = '2018-01-05'
logger = logging.getLogger(__name__)
class Group(object):
class Group:
"""
A group is simply a database group associated with its authenticator instance
It's only constructor expect a database group as parameter.
"""
def __init__(self, dbGroup):
def __init__(self, dbGroup: DBGroup):
"""
Initializes internal data
"""
self._manager = dbGroup.getManager()
self._dbGroup = dbGroup
def manager(self):
def manager(self) -> 'AuthenticatorInstance':
"""
Returns the database authenticator associated with this group
"""
return self._manager
def dbGroup(self):
def dbGroup(self) -> DBGroup:
"""
Returns the database group associated with this
"""

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,23 +30,22 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
import re
import logging
import typing
from collections.abc import Iterable
from uds.core.util.State import State
from uds.models import Group as dbGroup
from uds.core.auths.Group import Group
from uds.models import Group as DBGroup
from uds.models import Authenticator as DBAuthenticator
from .Group import Group
import six
import re
import inspect
import logging
__updated__ = '2018-02-01'
logger = logging.getLogger(__name__)
class GroupsManager(object):
class GroupsManager:
"""
Manages registered groups for an specific authenticator.
@ -65,8 +64,9 @@ class GroupsManager(object):
Managed groups names are compared using case insensitive comparison.
"""
_groups: typing.Dict[str, dict]
def __init__(self, dbAuthenticator):
def __init__(self, dbAuthenticator: DBAuthenticator):
"""
Initializes the groups manager.
The dbAuthenticator is the database record of the authenticator
@ -79,74 +79,73 @@ class GroupsManager(object):
isPattern = name.find('pat:') == 0 # Is a pattern?
self._groups[name] = {'name': g.name, 'group': Group(g), 'valid': False, 'pattern': isPattern}
def checkAllGroups(self, groupName):
def checkAllGroups(self, groupName: str):
"""
Returns true if this groups manager contains the specified group name (string)
"""
name = groupName.lower()
res = []
for gName, grp in six.iteritems(self._groups):
for gName, grp in self._groups.items():
if grp['pattern'] is True:
logger.debug('Group is a pattern: {}'.format(grp))
logger.debug('Group is a pattern: %s', grp)
try:
logger.debug('Match: {}->{}'.format(grp['name'][4:], name))
logger.debug('Match: %s->%s', grp['name'][4:], name)
if re.search(grp['name'][4:], name, re.IGNORECASE) is not None:
res.append(grp) # Stop searching, one group at least matches
except Exception:
logger.exception('Exception in RE')
else:
logger.debug('Group: {}=={}'.format(name, gName))
logger.debug('Group: %s==%s', name, gName)
if name == gName:
res.append(grp)
return res
def getGroupsNames(self):
def getGroupsNames(self) -> typing.Iterable[str]:
"""
Return all groups names managed by this groups manager. The names are returned
as where inserted inside Database (most probably using administration interface)
"""
for g in six.itervalues(self._groups):
yield g['group'].dbGroup().name
for g in self._groups.values():
yield g['group'].DBGroup().name
def getValidGroups(self):
def getValidGroups(self) -> typing.Iterable[Group]:
"""
returns the list of valid groups (:py:class:uds.core.auths.Group.Group)
"""
lst = ()
for g in six.itervalues(self._groups):
lst: typing.List[str] = []
for g in self._groups.values():
if g['valid'] is True:
lst += (g['group'].dbGroup().id,)
lst += (g['group'].DBGroup().id,)
yield g['group']
# Now, get metagroups and also return them
for g in dbGroup.objects.filter(manager__id=self._dbAuthenticator.id, is_meta=True): # @UndefinedVariable
gn = g.groups.filter(id__in=lst, state=State.ACTIVE).count()
if g.meta_if_any is True and gn > 0:
gn = g.groups.count()
if gn == g.groups.count(): # If a meta group is empty, all users belongs to it. we can use gn != 0 to check that if it is empty, is not valid
for g2 in DBGroup.objects.filter(manager__id=self._dbAuthenticator.id, is_meta=True): # @UndefinedVariable
gn = g2.groups.filter(id__in=lst, state=State.ACTIVE).count()
if g2.meta_if_any is True and gn > 0:
gn = g2.groups.count()
if gn == g2.groups.count(): # If a meta group is empty, all users belongs to it. we can use gn != 0 to check that if it is empty, is not valid
# This group matches
yield Group(g)
yield Group(g2)
def hasValidGroups(self):
"""
Checks if this groups manager has at least one group that has been
validated (using :py:meth:.validate)
"""
for g in six.itervalues(self._groups):
for g in self._groups.values():
if g['valid'] is True:
return True
return False
def getGroup(self, groupName):
def getGroup(self, groupName: str) -> typing.Optional[Group]:
"""
If this groups manager contains that group manager, it returns the
:py:class:uds.core.auths.Group.Group representing that group name.
"""
if groupName.lower() in self._groups:
return self._groups[groupName.lower()]['group']
else:
return None
return None
def validate(self, groupName):
def validate(self, groupName: typing.Union[str, typing.Iterable]):
"""
Validates that the group groupName passed in is valid for this group manager.
@ -158,14 +157,14 @@ class GroupsManager(object):
Returns nothing, it changes the groups this groups contains attributes,
so they reflect the known groups that are considered valid.
"""
if type(groupName) is tuple or type(groupName) is list or inspect.isgenerator(groupName):
if isinstance(groupName, Iterable):
for n in groupName:
self.validate(n)
else:
for grp in self.checkAllGroups(groupName):
grp['valid'] = True
def isValid(self, groupName):
def isValid(self, groupName: str) -> bool:
"""
Checks if this group name is marked as valid inside this groups manager.
Returns True if group name is marked as valid, False if it isn't.

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,20 +30,30 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
import logging
import typing
from uds.models import Group as DBGroup, User as DBUser
from .GroupsManager import GroupsManager
from .Group import Group
# Imports for type checking
if typing.TYPE_CHECKING:
from uds.core.auths.BaseAuthenticator import Authenticator as AuthenticatorInstance
__updated__ = '2018-09-04'
logger = logging.getLogger(__name__)
class User(object):
class User:
"""
An user represents a database user, associated with its authenticator (instance)
and its groups.
"""
_manager: 'AuthenticatorInstance'
_grpsManager: typing.Optional[GroupsManager]
_dbUser: DBUser
_groups: typing.Optional[typing.List[Group]]
def __init__(self, dbUser):
self._manager = dbUser.getManager()
@ -51,19 +61,17 @@ class User(object):
self._dbUser = dbUser
self._groups = None
def _groupsManager(self):
def _groupsManager(self) -> GroupsManager:
"""
If the groups manager for this user already exists, it returns this.
If it does not exists, it creates one default from authenticator and
returns it.
"""
from .GroupsManager import GroupsManager
if self._grpsManager is None:
self._grpsManager = GroupsManager(self._manager.dbAuthenticator())
return self._grpsManager
def groups(self):
def groups(self) -> typing.List[Group]:
"""
Returns the valid groups for this user.
To do this, it will validate groups through authenticator instance using
@ -71,34 +79,31 @@ class User(object):
:note: Once obtained valid groups, it caches them until object removal.
"""
from uds.models import User as DbUser
from uds.core.auths.Group import Group
if self._groups is None:
if self._manager.isExternalSource is True:
self._manager.getGroups(self._dbUser.name, self._groupsManager())
self._groups = list(self._groupsManager().getValidGroups())
logger.debug(self._groups)
# This is just for updating "cached" data of this user, we only get real groups at login and at modify user operation
usr = DbUser.objects.get(pk=self._dbUser.id) # @UndefinedVariable
lst = ()
usr = DBUser.objects.get(pk=self._dbUser.id) # @UndefinedVariable
lst: typing.List[DBGroup] = []
for g in self._groups:
if g.dbGroup().is_meta is False:
lst += (g.dbGroup().id,)
usr.groups.set(lst)
else:
# From db
usr = DbUser.objects.get(pk=self._dbUser.id) # @UndefinedVariable
usr = DBUser.objects.get(pk=self._dbUser.id) # @UndefinedVariable
self._groups = [Group(g) for g in usr.getGroups()]
return self._groups
def manager(self):
def manager(self) -> 'AuthenticatorInstance':
"""
Returns the authenticator instance
"""
return self._manager
def dbUser(self):
def dbUser(self) -> DBUser:
"""
Returns the database user
"""

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -32,16 +32,12 @@ UDS authentication related interfaces and classes
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
from uds.core.auths.BaseAuthenticator import Authenticator
from uds.core.auths.User import User
from uds.core.auths.Group import Group
from uds.core.auths.GroupsManager import GroupsManager
from uds.core.auths import Exceptions
__updated__ = '2014-11-11'
def factory():
"""

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -33,10 +33,11 @@ Provides useful functions for authenticating, used by web interface.
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
'''
from __future__ import unicode_literals
import logging
import typing
from functools import wraps
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http import HttpResponseRedirect, HttpResponseForbidden, HttpResponse, HttpRequest
from django.utils.translation import get_language
from django.utils.decorators import available_attrs
from django.urls import reverse
@ -49,12 +50,9 @@ from uds.core import auths
from uds.core.util.stats import events
from uds.core.managers import cryptoManager
from uds.core.util.State import State
from uds.models import User
from uds.models import User, Authenticator
from uds.core.auths.BaseAuthenticator import Authenticator as AuthenticatorInstance
import logging
import six
__updated__ = '2019-01-21'
logger = logging.getLogger(__name__)
authLogger = logging.getLogger('authLog')
@ -64,7 +62,7 @@ PASS_KEY = 'pk'
ROOT_ID = -20091204 # Any negative number will do the trick
def getUDSCookie(request, response=None, force=False):
def getUDSCookie(request: HttpRequest, response: typing.Optional[HttpResponse] = None, force: bool = False) -> str:
'''
Generates a random cookie for uds, used, for example, to encript things
'''
@ -84,9 +82,10 @@ def getUDSCookie(request, response=None, force=False):
def getRootUser():
# pylint: disable=unexpected-keyword-arg, no-value-for-parameter
from uds.models import Authenticator
u = User(id=ROOT_ID, name=GlobalConfig.SUPER_USER_LOGIN.get(True), real_name=_('System Administrator'), state=State.ACTIVE, staff_member=True, is_admin=True)
u = User(id=ROOT_ID, name=GlobalConfig.SUPER_USER_LOGIN.get(True), real_name=_(
'System Administrator'), state=State.ACTIVE, staff_member=True, is_admin=True)
u.manager = Authenticator()
# Fake overwrite some methods, not too "legal" maybe? :)
u.getGroups = lambda: []
u.updateLastAccess = lambda: None
u.logout = lambda: None
@ -100,14 +99,13 @@ def getIp(request):
# Decorator to make easier protect pages that needs to be logged in
def webLoginRequired(admin=False):
def webLoginRequired(admin: bool = False):
"""
Decorator to set protection to access page
Look for samples at uds.core.web.views
"""
def decorator(view_func):
@wraps(view_func, assigned=available_attrs(view_func))
def _wrapped_view(request, *args, **kwargs):
"""
@ -117,7 +115,7 @@ def webLoginRequired(admin=False):
url = request.build_absolute_uri(GlobalConfig.LOGIN_URL.get())
if GlobalConfig.REDIRECT_TO_HTTPS.getBool() is True:
url = url.replace('http://', 'https://')
logger.debug('No user found, redirecting to {0}'.format(url))
logger.debug('No user found, redirecting to %s', url)
return HttpResponseRedirect(url)
if admin is True or admin == 'admin':
@ -163,7 +161,7 @@ def denyNonAuthenticated(view_func):
return _wrapped_view
def __registerUser(authenticator, authInstance, username):
def __registerUser(authenticator: Authenticator, authInstance: AuthenticatorInstance, username: str) -> typing.Optional[User]:
"""
Check if this user already exists on database with this authenticator, if don't, create it with defaults
This will work correctly with both internal or externals cause we first authenticate the user, if internal and user do not exists in database
@ -172,7 +170,7 @@ def __registerUser(authenticator, authInstance, username):
from uds.core.util.request import getRequest
username = authInstance.transformUsername(username)
logger.debug('Transformed username: {0}'.format(username))
logger.debug('Transformed username: %s', username)
request = getRequest()
@ -184,13 +182,14 @@ def __registerUser(authenticator, authInstance, username):
usr.getManager().recreateGroups(usr)
# And add an login event
events.addEvent(authenticator, events.ET_LOGIN, username=username, srcip=request.ip) # pylint: disable=maybe-no-member
events.addEvent(authenticator, events.ET_PLATFORM, platform=request.os.OS, browser=request.os.Browser, version=request.os.Version) # pylint: disable=maybe-no-member
events.addEvent(authenticator, events.ET_PLATFORM, platform=request.os.OS, browser=request.os.Browser,
version=request.os.Version) # pylint: disable=maybe-no-member
return usr
return None
def authenticate(username, password, authenticator, useInternalAuthenticate=False):
def authenticate(username: str, password: str, authenticator: Authenticator, useInternalAuthenticate: bool = False) -> typing.Optional[User]:
"""
Given an username, password and authenticator, try to authenticate user
@param username: username to authenticate
@ -200,7 +199,7 @@ def authenticate(username, password, authenticator, useInternalAuthenticate=Fals
This is so because in some situations we may want to use a "trusted" method (internalAuthenticate is never invoked directly from web)
@return: None if authentication fails, User object (database object) if authentication is o.k.
"""
logger.debug('Authenticating user {0} with authenticator {1}'.format(username, authenticator))
logger.debug('Authenticating user %s with authenticator %s', username, authenticator)
# If global root auth is enabled && user/password is correct,
if GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True) and username == GlobalConfig.SUPER_USER_LOGIN.get(True) and password == GlobalConfig.SUPER_USER_PASS.get(True):
@ -216,7 +215,7 @@ def authenticate(username, password, authenticator, useInternalAuthenticate=Fals
if res is False:
return None
logger.debug('Groups manager: {0}'.format(gm))
logger.debug('Groups manager: %s', gm)
# If do not have any valid group
if gm.hasValidGroups() is False:
@ -226,7 +225,7 @@ def authenticate(username, password, authenticator, useInternalAuthenticate=Fals
return __registerUser(authenticator, authInstance, username)
def authenticateViaCallback(authenticator, params):
def authenticateViaCallback(authenticator: Authenticator, params: typing.Any) -> typing.Optional[User]:
"""
Given an username, this method will get invoked whenever the url for a callback
for an authenticator is requested.
@ -260,26 +259,28 @@ def authenticateViaCallback(authenticator, params):
return __registerUser(authenticator, authInstance, username)
def authCallbackUrl(authenticator):
def authCallbackUrl(authenticator) -> str:
"""
Helper method, so we can get the auth call back url for an authenticator
"""
return reverse('page.auth.callback', kwargs={'authName': authenticator.name})
def authInfoUrl(authenticator):
def authInfoUrl(authenticator: typing.Union[str, bytes, Authenticator]) -> str:
"""
Helper method, so we can get the info url for an authenticator
"""
if isinstance(authenticator, (six.text_type, six.binary_type)):
if isinstance(authenticator, str):
name = authenticator
elif isinstance(authenticator, bytes):
name = authenticator.decode('utf8')
else:
name = authenticator.name
return reverse('page.auth.info', kwargs={'authName': name})
def webLogin(request, response, user, password):
def webLogin(request: HttpRequest, response: HttpResponse, user: User, password: str) -> bool:
"""
Helper function to, once the user is authenticated, store the information at the user session.
@return: Always returns True
@ -303,7 +304,7 @@ def webLogin(request, response, user, password):
return True
def webPassword(request):
def webPassword(request: HttpRequest) -> str:
"""
The password is stored at session using a simple scramble algorithm that keeps the password splited at
session (db) and client browser cookies. This method uses this two values to recompose the user password
@ -314,19 +315,21 @@ def webPassword(request):
return cryptoManager().symDecrpyt(request.session.get(PASS_KEY, ''), getUDSCookie(request)) # recover as original unicode string
def webLogout(request, exit_url=None):
def webLogout(request: HttpRequest, exit_url: typing.Optional[str] = None) -> HttpResponse:
"""
Helper function to clear user related data from session. If this method is not used, the session we be cleaned anyway
by django in regular basis.
"""
authenticator = request.user and request.user.manager.getInstance() or None
username = request.user and request.user.name or None
exit_url = authenticator.logout(username) or exit_url
if request.user is not None and request.user.id != ROOT_ID:
# Try yo invoke logout of auth
events.addEvent(request.user.manager, events.ET_LOGOUT, username=request.user.name, srcip=request.ip)
if request.user:
authenticator = request.user.manager.getInstance()
username = request.user.name
exit_url = authenticator.logout(username) or exit_url
if request.user.id != ROOT_ID:
# Try yo invoke logout of auth
events.addEvent(request.user.manager, events.ET_LOGOUT, username=request.user.name, srcip=request.ip)
else: # No user, redirect to /
return HttpResponseRedirect(reverse('page.login'))
request.session.clear()
if exit_url is None:
@ -341,7 +344,7 @@ def webLogout(request, exit_url=None):
return response
def authLogLogin(request, authenticator, userName, logStr=''):
def authLogLogin(request: HttpRequest, authenticator: Authenticator, userName: str, logStr: str = '') -> None:
"""
Logs authentication
"""
@ -355,13 +358,11 @@ def authLogLogin(request, authenticator, userName, logStr=''):
try:
user = authenticator.users.get(name=userName)
log.doLog(user, level,
'{} from {} where OS is {}'.format(logStr, request.ip, request.os['OS']), log.WEB
)
log.doLog(user, level, '{} from {} where OS is {}'.format(logStr, request.ip, request.os['OS']), log.WEB)
except Exception:
pass
def authLogLogout(request):
def authLogLogout(request: HttpRequest):
log.doLog(request.user.manager, log.INFO, 'user {0} has logged out from {1}'.format(request.user.name, request.ip), log.WEB)
log.doLog(request.user, log.INFO, 'has logged out from {0}'.format(request.ip), log.WEB)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,12 +30,10 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
from uds.core.Environment import Environmentable
import logging
__updated__ = '2018-09-17'
from uds.core.Environment import Environmentable
logger = logging.getLogger(__name__)
@ -64,7 +62,7 @@ class DelayedTask(Environmentable):
"""
logging.debug("Base run of job called for class")
def register(self, suggestedTime, tag='', check=True):
def register(self, suggestedTime, tag: str = '', check: bool = True):
"""
Utility method that allows to register a Delayedtask
"""

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -32,13 +32,9 @@ UDS jobs related modules
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
from uds.core.jobs.Job import Job
from uds.core.jobs.DelayedTask import DelayedTask
__updated__ = '2014-11-11'
def factory():
"""

View File

@ -30,25 +30,25 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
from django.conf import settings
from uds.core.util import encoders
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES
from uds.core.util import encoders
from OpenSSL import crypto
from Crypto.Random import atfork
import typing
import hashlib
import array
import uuid
import struct
import datetime
import random
import string
import logging
import six
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES
# from Crypto.Random import atfork
from OpenSSL import crypto
from django.conf import settings
from uds.core.util import encoders
logger = logging.getLogger(__name__)
@ -59,7 +59,7 @@ logger = logging.getLogger(__name__)
# RSA.generate(1024, os.urandom).exportKey()
class CryptoManager(object):
class CryptoManager:
instance = None
def __init__(self):
@ -68,60 +68,60 @@ class CryptoManager(object):
self._counter = 0
@staticmethod
def AESKey(key, length):
if isinstance(key, six.text_type):
def AESKey(key: typing.Union[str, bytes], length: int) -> bytes:
if isinstance(key, str):
key = key.encode('utf8')
while len(key) < length:
key += key # Dup key
kl = [ord(v) for v in key]
kl: typing.List[int] = [v for v in key]
pos = 0
while len(kl) > length:
kl[pos] ^= kl[length]
pos = (pos + 1) % length
del kl[length]
return b''.join([chr(v) for v in kl])
return bytes(kl)
@staticmethod
def manager():
def manager() -> 'CryptoManager':
if CryptoManager.instance is None:
CryptoManager.instance = CryptoManager()
return CryptoManager.instance
def encrypt(self, value):
if isinstance(value, six.text_type):
def encrypt(self, value: typing.Union[str, bytes]) -> str:
if isinstance(value, str):
value = value.encode('utf-8')
atfork()
return encoders.encode((self._rsa.encrypt(value, six.b(''))[0]), 'base64', asText=True)
# atfork()
return encoders.encode((self._rsa.encrypt(value, b'')[0]), 'base64', asText=True)
def decrypt(self, value):
if isinstance(value, six.text_type):
def decrypt(self, value: typing.Union[str, bytes]) -> str:
if isinstance(value, str):
value = value.encode('utf-8')
# import inspect
try:
atfork()
return six.text_type(self._rsa.decrypt(encoders.decode(value, 'base64')).decode('utf-8'))
# atfork()
return str(self._rsa.decrypt(encoders.decode(value, 'base64')).decode('utf-8'))
except Exception:
logger.exception('Decripting: {0}'.format(value))
logger.exception('Decripting: %s', value)
# logger.error(inspect.stack())
return 'decript error'
def AESCrypt(self, text, key, base64=False):
def AESCrypt(self, text: bytes, key: bytes, base64: bool = False) -> bytes:
# First, match key to 16 bytes. If key is over 16, create a new one based on key of 16 bytes length
cipher = AES.new(CryptoManager.AESKey(key, 16), AES.MODE_CBC, 'udsinitvectoruds')
rndStr = self.randomString(cipher.block_size)
paddedLength = ((len(text) + 4 + 15) // 16) * 16
toEncode = struct.pack('>i', len(text)) + text + rndStr[:paddedLength - len(text) - 4]
encoded = cipher.encrypt(toEncode)
if hex:
return encoders.encode(encoded, 'base64', True)
if base64:
return encoders.encode(encoded, 'base64', asText=False) # Return as binary
return encoded
def AESDecrypt(self, text, key, base64=False):
def AESDecrypt(self, text: bytes, key: bytes, base64: bool = False) -> bytes:
if base64:
text = encoders.decode(text, 'base64')
@ -129,23 +129,21 @@ class CryptoManager(object):
toDecode = cipher.decrypt(text)
return toDecode[4:4 + struct.unpack('>i', toDecode[:4])[0]]
return
def xor(self, s1, s2):
def xor(self, s1: typing.Union[str, bytes], s2: typing.Union[str, bytes]) -> bytes:
if isinstance(s1, str):
s1 = s1.encode('utf-8')
if isinstance(s2, str):
s2 = s2.encode('utf-8')
mult = int(len(s1) / len(s2)) + 1
s1 = array.array('B', s1)
s2 = array.array('B', s2 * mult)
s1a = array.array('B', s1)
s2a = array.array('B', s2 * mult)
# We must return bynary in xor, because result is in fact binary
return array.array('B', (s1[i] ^ s2[i] for i in range(len(s1)))).tobytes()
return array.array('B', (s1a[i] ^ s2a[i] for i in range(len(s1a)))).tobytes()
def symCrypt(self, text, key):
def symCrypt(self, text: typing.Union[str, bytes], key: typing.Union[str, bytes]) -> bytes:
return self.xor(text, key)
def symDecrpyt(self, cryptText, key):
def symDecrpyt(self, cryptText: typing.Union[str, bytes], key: typing.Union[str, bytes]) -> str:
return self.xor(cryptText, key).decode('utf-8')
def loadPrivateKey(self, rsaKey):
@ -165,14 +163,14 @@ class CryptoManager(object):
def certificateString(self, certificate):
return certificate.replace('-----BEGIN CERTIFICATE-----', '').replace('-----END CERTIFICATE-----', '').replace('\n', '')
def hash(self, value):
if isinstance(value, six.text_type):
def hash(self, value: typing.Union[str, bytes]) -> str:
if isinstance(value, str):
value = value.encode('utf-8')
if value is '' or value is None:
if not value:
return ''
return six.text_type(hashlib.sha1(value).hexdigest())
return str(hashlib.sha1(value).hexdigest())
def uuid(self, obj=None):
"""

View File

@ -86,8 +86,9 @@ class TaskManager(object):
def registerScheduledTasks():
logger.info("Registering sheduled tasks")
# Simply import this to make workers "auto import"
from uds.core import workers # @UnusedImport
# Simply import this to make workers "auto imported"
from uds.core import workers # @UnusedImport pylint: disable=unused-import
@staticmethod
def run():

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -32,40 +32,42 @@ UDS managers (downloads, users preferences, publications, ...)
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
import typing
__updated__ = '2019-02-05'
def cryptoManager():
':rtype uds.core.managers.CryptoManager.CryptoManager'
# Imports for type checking
if typing.TYPE_CHECKING:
from .CryptoManager import CryptoManager
from .TaskManager import TaskManager
from .DownloadsManager import DownloadsManager
from .LogManager import LogManager
from .StatsManager import StatsManager
from .UserServiceManager import UserServiceManager
def cryptoManager() -> 'CryptoManager':
from .CryptoManager import CryptoManager # pylint: disable=redefined-outer-name
return CryptoManager.manager()
def taskManager():
from .TaskManager import TaskManager
from .TaskManager import TaskManager # pylint: disable=redefined-outer-name
return TaskManager
def downloadsManager():
from .DownloadsManager import DownloadsManager
from .DownloadsManager import DownloadsManager # pylint: disable=redefined-outer-name
return DownloadsManager.manager()
def logManager():
':rtype uds.core.managers.LogManager.LogManager'
from .LogManager import LogManager
from .LogManager import LogManager # pylint: disable=redefined-outer-name
return LogManager.manager()
def statsManager():
':rtype uds.core.managers.StatsManager.StatsManager'
from .StatsManager import StatsManager
from .StatsManager import StatsManager # pylint: disable=redefined-outer-name
return StatsManager.manager()
def userServiceManager():
':rtype uds.core.managers.UserServiceManager.UserServiceManager'
from .UserServiceManager import UserServiceManager
from .UserServiceManager import UserServiceManager # pylint: disable=redefined-outer-name
return UserServiceManager.manager()

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -30,18 +30,22 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from django.utils.translation import get_language, ugettext as _, ugettext_noop
from uds.core.util import encoders
import datetime
import typing
import time
import six
import pickle
import logging
# import six
from django.utils.translation import get_language, ugettext as _, ugettext_noop
from uds.core.util import encoders
logger = logging.getLogger(__name__)
class gui(object):
class gui:
"""
This class contains the representations of fields needed by UDS modules and
administation interface.
@ -89,7 +93,7 @@ class gui(object):
DISPLAY_TAB = ugettext_noop('Display')
# : Static Callbacks simple registry
callbacks = {}
callbacks: typing.Dict[str, typing.Callable] = {}
# Helpers
@staticmethod
@ -107,7 +111,7 @@ class gui(object):
@staticmethod
def convertToList(vals):
if vals is not None:
return [six.text_type(v) for v in vals]
return [str(v) for v in vals]
return []
@staticmethod
@ -127,11 +131,11 @@ class gui(object):
:note: Text can be anything, the method converts it first to text before
assigning to dictionary
"""
return {'id': str(id_), 'text': six.text_type(text)}
return {'id': str(id_), 'text': str(text)}
@staticmethod
def choiceImage(id_, text, img):
return {'id': str(id_), 'text': six.text_type(text), 'img': img }
return {'id': str(id_), 'text': str(text), 'img': img }
@staticmethod
def sortedChoices(choices):
@ -151,7 +155,7 @@ class gui(object):
"""
if isinstance(str_, bool):
return str_
if six.text_type(str_).lower() == gui.TRUE:
if str(str_).lower() == gui.TRUE:
return True
return False
@ -230,7 +234,7 @@ class gui(object):
'length': options.get('length', gui.InputField.DEFAULT_LENTGH),
'required': options.get('required', False),
'label': options.get('label', ''),
'defvalue': six.text_type(options.get('defvalue', '')),
'defvalue': str(options.get('defvalue', '')),
'rdonly': options.get('rdonly', False), # This property only affects in "modify" operations
'order': options.get('order', 0),
'tooltip': options.get('tooltip', ''),
@ -349,7 +353,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._type(gui.InputField.TEXT_TYPE)
multiline = int(options.get('multiline', 0))
if multiline > 8:
@ -378,7 +382,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
minValue = options.get('minValue', '987654321')
maxValue = options.get('maxValue', '987654321')
self._data['minValue'] = int(minValue)
@ -432,7 +436,7 @@ class gui(object):
for v in 'value', 'defvalue':
self.processValue(v, options)
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._type(gui.InputField.DATE_TYPE)
def date(self):
@ -467,7 +471,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._type(gui.InputField.PASSWORD_TYPE)
class HiddenField(InputField):
@ -503,7 +507,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._isSerializable = options.get('serializable', '') != ''
self._type(gui.InputField.HIDDEN_TYPE)
@ -531,7 +535,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._type(gui.InputField.CHECKBOX_TYPE)
@staticmethod
@ -645,7 +649,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._data['values'] = options.get('values', [])
if 'fills' in options:
# Save fnc to register as callback
@ -665,7 +669,7 @@ class gui(object):
class ImageChoiceField(InputField):
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._data['values'] = options.get('values', [])
self._type(gui.InputField.IMAGECHOICE_TYPE)
@ -711,7 +715,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._data['values'] = options.get('values', [])
self._data['rows'] = options.get('rows', -1)
self._type(gui.InputField.MULTI_CHOICE_TYPE)
@ -753,16 +757,16 @@ class gui(object):
SEPARATOR = '\001'
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._data['values'] = gui.convertToList(options.get('values', []))
self._type(gui.InputField.EDITABLE_LIST)
def _setValue(self, values):
def _setValue(self, value):
"""
So we can override value setting at descendants
"""
super(self.__class__, self)._setValue(values)
self._data['values'] = gui.convertToList(values)
super()._setValue(value)
self._data['values'] = gui.convertToList(value)
class ImageField(InputField):
"""
@ -770,7 +774,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._type(gui.InputField.TEXT_TYPE)
class InfoField(InputField):
@ -779,7 +783,7 @@ class gui(object):
"""
def __init__(self, **options):
super(self.__class__, self).__init__(**options)
super().__init__(**options)
self._type(gui.InputField.INFO_TYPE)
@ -789,7 +793,7 @@ class UserInterfaceType(type):
better place
"""
def __new__(cls, classname, bases, classDict):
def __new__(cls, classname, bases, classDict): # pylint: disable=bad-mcs-classmethod-argument
newClassDict = {}
_gui = {}
# We will keep a reference to gui elements also at _gui so we can access them easily
@ -801,8 +805,7 @@ class UserInterfaceType(type):
return type.__new__(cls, classname, bases, newClassDict)
# @six.add_metaclass(UserInterfaceType)
class UserInterface(object, metaclass=UserInterfaceType):
class UserInterface(metaclass=UserInterfaceType):
"""
This class provides the management for gui descriptions (user forms)
@ -813,7 +816,6 @@ class UserInterface(object, metaclass=UserInterfaceType):
By default, the values passed to this class constructor are used to fill
the gui form fields values.
"""
# __metaclass__ = UserInterfaceType
def __init__(self, values=None):
import copy
@ -821,15 +823,15 @@ class UserInterface(object, metaclass=UserInterfaceType):
# Generate a deep copy of inherited Gui, so each User Interface instance has its own "field" set, and do not share the "fielset" with others, what can be really dangerous
# Till now, nothing bad happened cause there where being used "serialized", but this do not have to be this way
self._gui = copy.deepcopy(self._gui) # Ensure "gui" is our own instance, deep copied from base
for key, val in six.iteritems(self._gui): # And refresh references to them
for key, val in self._gui.items(): # And refresh references to them
setattr(self, key, val)
if values is not None:
for k, v in six.iteritems(self._gui):
for k, v in self._gui.items():
if k in values:
v.value = values[k]
else:
logger.warning('Field {} not found'.format(k))
logger.warning('Field %s not found', k)
def initGui(self):
"""
@ -850,7 +852,6 @@ class UserInterface(object, metaclass=UserInterfaceType):
time, and returned data will be probable a nonsense. We will take care
of this posibility in a near version...
"""
pass
def valuesDict(self):
"""
@ -880,7 +881,7 @@ class UserInterface(object, metaclass=UserInterfaceType):
"""
dic = {}
for k, v in six.iteritems(self._gui):
for k, v in self._gui.items():
if v.isType(gui.InputField.EDITABLE_LIST):
dic[k] = gui.convertToList(v.value)
elif v.isType(gui.InputField.MULTI_CHOICE_TYPE):
@ -891,7 +892,7 @@ class UserInterface(object, metaclass=UserInterfaceType):
# dic[k] = v.defValue
else:
dic[k] = v.value
logger.debug('Values Dict: {0}'.format(dic))
logger.debug('Values Dict: %s', dic)
return dic
def serializeForm(self):
@ -911,7 +912,7 @@ class UserInterface(object, metaclass=UserInterfaceType):
arr = []
for k, v in self._gui.items():
logger.debug('serializing Key: {0}/{1}'.format(k, v.value))
logger.debug('serializing Key: %s/%s', k, v.value)
if v.isType(gui.InputField.HIDDEN_TYPE) and v.isSerializable() is False:
# logger.debug('Field {0} is not serializable'.format(k))
continue
@ -947,7 +948,7 @@ class UserInterface(object, metaclass=UserInterfaceType):
try:
# Set all values to defaults ones
for k in six.iterkeys(self._gui):
for k in self._gui:
if self._gui[k].isType(gui.InputField.HIDDEN_TYPE) and self._gui[k].isSerializable() is False:
# logger.debug('Field {0} is not unserializable'.format(k))
continue
@ -975,7 +976,7 @@ class UserInterface(object, metaclass=UserInterfaceType):
self._gui[k].value = val
# logger.debug('Value for {0}:{1}'.format(k, val))
except Exception:
logger.exception('Exception on unserialization on {}'.format(self.__class__))
logger.exception('Exception on unserialization on %s', self.__class__)
# Values can contain invalid characters, so we log every single char
# logger.info('Invalid serialization data on {0} {1}'.format(self, values.encode('hex')))
@ -990,7 +991,7 @@ class UserInterface(object, metaclass=UserInterfaceType):
object: If not none, object that will get its "initGui" invoked
This will only happen (not to be None) in Services.
"""
logger.debug('Active language for theGui translation: {0}'.format(get_language()))
logger.debug('Active language for theGui translation: %s', get_language())
theGui = cls
if obj is not None:
obj.initGui() # We give the "oportunity" to fill necesary theGui data before providing it to client
@ -998,9 +999,9 @@ class UserInterface(object, metaclass=UserInterfaceType):
res = []
# pylint: disable=protected-access,maybe-no-member
for key, val in six.iteritems(theGui._gui):
logger.debug('{0} ### {1}'.format(key, val))
for key, val in theGui._gui.items():
logger.debug('%s ### %s', key, val)
res.append({'name': key, 'gui': val.guiDescription(), 'value': ''})
logger.debug('>>>>>>>>>>>> Gui Description: {0} -- {1}'.format(obj, res))
logger.debug('>>>>>>>>>>>> Gui Description: %s -- %s', obj, res)
return res

View File

@ -5,4 +5,4 @@ This module contains the definition of UserInterface, needed to describe the int
between an UDS module and the administration interface
"""
from uds.core.ui.UserInterface import gui
from .UserInterface import gui

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,

View File

@ -30,13 +30,10 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
import six
import codecs
def __toBinary(data):
if isinstance(data, six.text_type):
if isinstance(data, str):
return data.encode('utf8')
return data

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2016 Virtual Cable S.L.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -29,24 +29,22 @@
"""
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
"""
from __future__ import unicode_literals
import threading
import logging
import typing
from uds.core.util import OsDetector
from uds.core.util.Config import GlobalConfig
from uds.core.auths.auth import ROOT_ID, USER_KEY, getRootUser
from uds.models import User
import threading
import logging
__updated__ = '2017-01-11'
logger = logging.getLogger(__name__)
_requests = {}
_requests : typing.Dict[int, typing.Any] = {}
def getIdent():
def getIdent() -> typing.Optional[int]:
return threading.current_thread().ident
@ -57,8 +55,7 @@ def getRequest():
return {}
class GlobalRequestMiddleware(object):
class GlobalRequestMiddleware:
def __init__(self, get_response):
self.get_response = get_response
@ -70,20 +67,20 @@ class GlobalRequestMiddleware(object):
# Ensures that requests contains the valid user
GlobalRequestMiddleware.getUser(request)
# Add a counter var, reseted on every request
# Store request on cache
_requests[getIdent()] = request
return None
def _process_response(self, request, response):
# Remove IP from global cache (processing responses after this will make global request unavailable,
# but can be got from request again)
ident = getIdent()
logger.debug('Deleting {}'.format(ident))
logger.debug('Deleting %s', ident)
try:
if ident in _requests:
del _requests[ident]
del _requests[ident] # Remove stored request
else:
logger.info('Request id {} not stored'.format(ident))
logger.info('Request id %s not stored in cache', ident)
except Exception:
logger.exception('Deleting stored request')
return response
@ -105,7 +102,7 @@ class GlobalRequestMiddleware(object):
behind_proxy = GlobalConfig.BEHIND_PROXY.getBool(False)
try:
request.ip = request.META['REMOTE_ADDR']
except:
except Exception:
logger.exception('Request ip not found!!')
request.ip = '0.0.0.0' # No remote addr?? set this IP to a "basic" one, anyway, this should never ocur
@ -117,7 +114,7 @@ class GlobalRequestMiddleware(object):
request.ip = request.ip_proxy # Stores the ip
# will raise "list out of range", leaving ip_proxy = proxy in case of no other proxy apart of nginx
request.ip_proxy = proxies[1]
except:
except Exception:
request.ip_proxy = request.ip
@staticmethod
@ -127,7 +124,7 @@ class GlobalRequestMiddleware(object):
"""
logger.debug('Getting User on Middleware')
user = request.session.get(USER_KEY)
if user is not None:
if user:
try:
if user == ROOT_ID:
user = getRootUser()
@ -136,7 +133,4 @@ class GlobalRequestMiddleware(object):
except User.DoesNotExist:
user = None
if user is not None:
request.user = user
else:
request.user = None
request.user = user

View File

@ -38,6 +38,7 @@ from django.db.models import signals
from uds.core.util.State import State
from uds.core.util import log
from uds.core.auths.BaseAuthenticator import Authenticator as AuthenticatorInstance
from .UUIDModel import UUIDModel
from .Authenticator import Authenticator
@ -62,7 +63,7 @@ class Group(UUIDModel):
groups = models.ManyToManyField('self', symmetrical=False)
created = models.DateTimeField(default=getSqlDatetime, blank=True)
class Meta(UUIDModel.Meta):
class Meta:
"""
Meta class to declare default order and unique multiple field index
"""
@ -71,10 +72,10 @@ class Group(UUIDModel):
app_label = 'uds'
@property
def pretty_name(self):
def pretty_name(self) -> str:
return self.name + '@' + self.manager.name
def getManager(self):
def getManager(self) -> AuthenticatorInstance:
"""
Returns the authenticator object that owns this user.
@ -107,7 +108,7 @@ class Group(UUIDModel):
# Clears related logs
log.clearLogs(toDelete)
logger.debug('Deleted group {0}'.format(toDelete))
logger.debug('Deleted group %s', toDelete)
signals.pre_delete.connect(Group.beforeDelete, sender=Group)