import csv import os from base64 import encodestring from hashlib import md5, sha1 from io import StringIO from os import urandom from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.core.mail import send_mail from django.db import models from django.db.models import Count, ProtectedError from django.utils.crypto import get_random_string from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ from openwisp_utils.base import TimeStampedEditableModel from passlib.hash import lmhash, nthash, sha512_crypt from django_freeradius.settings import ( BATCH_DEFAULT_PASSWORD_LENGTH, BATCH_MAIL_MESSAGE, BATCH_MAIL_SENDER, BATCH_MAIL_SUBJECT, ) from django_freeradius.utils import ( find_available_username, generate_pdf, prefix_generate_users, validate_csvfile, ) from .. import settings as app_settings from .validators import ipv6_network_validator User = get_user_model() RADOP_CHECK_TYPES = (('=', '='), (':=', ':='), ('==', '=='), ('+=', '+='), ('!=', '!='), ('>', '>'), ('>=', '>='), ('<', '<'), ('<=', '<='), ('=~', '=~'), ('!~', '!~'), ('=*', '=*'), ('!*', '!*')) RAD_NAS_TYPES = app_settings.EXTRA_NAS_TYPES + ( ('Async', 'Async'), ('Sync', 'Sync'), ('ISDN Sync', 'ISDN Sync'), ('ISDN Async V.120', 'ISDN Async V.120'), ('ISDN Async V.110', 'ISDN Async V.110'), ('Virtual', 'Virtual'), ('PIAFS', 'PIAFS'), ('HDLC Clear', 'HDLC Clear'), ('Channel', 'Channel'), ('X.25', 'X.25'), ('X.75', 'X.75'), ('G.3 Fax', 'G.3 Fax'), ('SDSL', 'SDSL - Symmetric DSL'), ('ADSL-CAP', 'ADSL-CAP'), ('ADSL-DMT', 'ADSL-DMT'), ('IDSL', 'IDSL'), ('Ethernet', 'Ethernet'), ('xDSL', 'xDSL'), ('Cable', 'Cable'), ('Wireless - Other', 'Wireless - Other'), ('IEEE 802.11', 'Wireless - IEEE 802.11'), ('Token-Ring', 'Token-Ring'), ('FDDI', 'FDDI'), ('Wireless - CDMA2000', 'Wireless - CDMA2000'), ('Wireless - UMTS', 'Wireless - UMTS'), ('Wireless - 1X-EV', 'Wireless - 1X-EV'), ('IAPP', 'IAPP'), ('FTTP', 'FTTP'), ('IEEE 802.16', 'Wireless - IEEE 802.16'), ('IEEE 802.20', 'Wireless - IEEE 802.20'), ('IEEE 802.22', 'Wireless - IEEE 802.22'), ('PPPoA', 'PPPoA - PPP over ATM'), ('PPPoEoA', 'PPPoEoA - PPP over Ethernet over ATM'), ('PPPoEoE', 'PPPoEoE - PPP over Ethernet over Ethernet'), ('PPPoEoVLAN', 'PPPoEoVLAN - PPP over Ethernet over VLAN'), ('PPPoEoQinQ', 'PPPoEoQinQ - PPP over Ethernet over IEEE 802.1QinQ'), ('xPON', 'xPON - Passive Optical Network'), ('Wireless - XGP', 'Wireless - XGP'), ('WiMAX', ' WiMAX Pre-Release 8 IWK Function'), ('WIMAX-WIFI-IWK', 'WIMAX-WIFI-IWK: WiMAX WIFI Interworking'), ('WIMAX-SFF', 'WIMAX-SFF: Signaling Forwarding Function for LTE/3GPP2'), ('WIMAX-HA-LMA', 'WIMAX-HA-LMA: WiMAX HA and or LMA function'), ('WIMAX-DHCP', 'WIMAX-DHCP: WIMAX DCHP service'), ('WIMAX-LBS', 'WIMAX-LBS: WiMAX location based service'), ('WIMAX-WVS', 'WIMAX-WVS: WiMAX voice service'), ('Other', 'Other'), ) RADOP_REPLY_TYPES = (('=', '='), (':=', ':='), ('+=', '+=')) RADCHECK_ATTRIBUTE_TYPES = ['Max-Daily-Session', 'Max-All-Session', 'Max-Daily-Session-Traffic'] RADCHECK_PASSWD_TYPE = ['Cleartext-Password', 'NT-Password', 'LM-Password', 'MD5-Password', 'SMD5-Password', 'SHA-Password', 'SSHA-Password', 'Crypt-Password'] RADCHECK_ATTRIBUTE_TYPES += RADCHECK_PASSWD_TYPE STRATEGIES = ( ('prefix', _('Generate from prefix')), ('csv', _('Import from CSV')) ) class BaseModel(TimeStampedEditableModel): id = None class Meta: abstract = True _NOT_BLANK_MESSAGE = _('This field cannot be blank.') class AutoUsernameMixin(object): def clean(self): """ automatically sets username """ if self.user: self.username = self.user.username elif not self.username: raise ValidationError({ 'username': _NOT_BLANK_MESSAGE, 'user': _NOT_BLANK_MESSAGE }) class AutoGroupnameMixin(object): def clean(self): """ automatically sets groupname """ super().clean() if self.group: self.groupname = self.group.name elif not self.groupname: raise ValidationError({ 'groupname': _NOT_BLANK_MESSAGE, 'group': _NOT_BLANK_MESSAGE }) class AbstractRadiusCheckQueryset(models.query.QuerySet): def filter_duplicate_username(self): pks = [] for i in self.values('username').annotate(Count('id')).order_by().filter(id__count__gt=1): pks.extend([account.pk for account in self.filter(username=i['username'])]) return self.filter(pk__in=pks) def filter_duplicate_value(self): pks = [] for i in self.values('value').annotate(Count('id')).order_by().filter(id__count__gt=1): pks.extend([accounts.pk for accounts in self.filter(value=i['value'])]) return self.filter(pk__in=pks) def filter_expired(self): return self.filter(valid_until__lt=now()) def filter_not_expired(self): return self.filter(valid_until__gte=now()) def _encode_secret(attribute, new_value=None): if attribute == 'NT-Password': attribute_value = nthash.hash(new_value) elif attribute == 'LM-Password': attribute_value = lmhash.hash(new_value) elif attribute == 'MD5-Password': attribute_value = md5(new_value.encode('utf-8')).hexdigest() elif attribute == 'SMD5-Password': salt = urandom(4) hash = md5(new_value.encode('utf-8')) hash.update(salt) hash_encoded = encodestring(hash.digest() + salt) attribute_value = hash_encoded.decode('utf-8')[:-1] elif attribute == 'SHA-Password': attribute_value = sha1(new_value.encode('utf-8')).hexdigest() elif attribute == 'SSHA-Password': salt = urandom(4) hash = sha1(new_value.encode('utf-8')) hash.update(salt) hash_encoded = encodestring(hash.digest() + salt) attribute_value = hash_encoded.decode('utf-8')[:-1] elif attribute == 'Crypt-Password': attribute_value = sha512_crypt.hash(new_value) else: attribute_value = new_value return attribute_value class AbstractRadiusCheckManager(models.Manager): def get_queryset(self): return AbstractRadiusCheckQueryset(self.model, using=self._db) def create(self, *args, **kwargs): if 'new_value' in kwargs: kwargs['value'] = _encode_secret(kwargs['attribute'], kwargs['new_value']) del(kwargs['new_value']) return super(AbstractRadiusCheckManager, self).create(*args, **kwargs) class AbstractRadiusCheck(AutoUsernameMixin, BaseModel): username = models.CharField(verbose_name=_('username'), max_length=64, db_index=True, # blank values are forbidden with custom validation # because this field can left blank if the user # foreign key is filled (it will be auto-filled) blank=True) value = models.CharField(verbose_name=_('value'), max_length=253) op = models.CharField(verbose_name=_('operator'), max_length=2, choices=RADOP_CHECK_TYPES, default=':=') attribute = models.CharField(verbose_name=_('attribute'), max_length=64, choices=[(i, i) for i in RADCHECK_ATTRIBUTE_TYPES if i not in app_settings.DISABLED_SECRET_FORMATS], default=app_settings.DEFAULT_SECRET_FORMAT) # the foreign key is not part of the standard freeradius schema user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True) # additional fields to enable more granular checks is_active = models.BooleanField(default=True) valid_until = models.DateTimeField(null=True, blank=True) # internal notes notes = models.TextField(null=True, blank=True) # custom manager objects = AbstractRadiusCheckManager() class Meta: db_table = 'radcheck' verbose_name = _('check') verbose_name_plural = _('checks') abstract = True def __str__(self): return self.username class AbstractRadiusReply(AutoUsernameMixin, BaseModel): username = models.CharField(verbose_name=_('username'), max_length=64, db_index=True, # blank values are forbidden with custom validation # because this field can left blank if the user # foreign key is filled (it will be auto-filled) blank=True) value = models.CharField(verbose_name=_('value'), max_length=253) op = models.CharField(verbose_name=_('operator'), max_length=2, choices=RADOP_REPLY_TYPES, default='=') attribute = models.CharField(verbose_name=_('attribute'), max_length=64) # the foreign key is not part of the standard freeradius schema user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True) class Meta: db_table = 'radreply' verbose_name = _('reply') verbose_name_plural = _('replies') abstract = True def __str__(self): return self.username class AbstractRadiusAccounting(models.Model): id = models.BigAutoField(primary_key=True, db_column='radacctid') session_id = models.CharField(verbose_name=_('session ID'), max_length=64, db_column='acctsessionid', db_index=True) unique_id = models.CharField(verbose_name=_('accounting unique ID'), max_length=32, db_column='acctuniqueid', unique=True) username = models.CharField(verbose_name=_('username'), max_length=64, db_index=True, null=True, blank=True) groupname = models.CharField(verbose_name=_('group name'), max_length=64, null=True, blank=True) realm = models.CharField(verbose_name=_('realm'), max_length=64, null=True, blank=True) nas_ip_address = models.GenericIPAddressField(verbose_name=_('NAS IP address'), db_column='nasipaddress', db_index=True) nas_port_id = models.CharField(verbose_name=_('NAS port ID'), max_length=15, db_column='nasportid', null=True, blank=True) nas_port_type = models.CharField(verbose_name=_('NAS port type'), max_length=32, db_column='nasporttype', null=True, blank=True) start_time = models.DateTimeField(verbose_name=_('start time'), db_column='acctstarttime', db_index=True, null=True, blank=True) update_time = models.DateTimeField(verbose_name=_('update time'), db_column='acctupdatetime', null=True, blank=True) stop_time = models.DateTimeField(verbose_name=_('stop time'), db_column='acctstoptime', db_index=True, null=True, blank=True) interval = models.IntegerField(verbose_name=_('interval'), db_column='acctinterval', null=True, blank=True) session_time = models.PositiveIntegerField(verbose_name=_('session time'), db_column='acctsessiontime', null=True, blank=True) authentication = models.CharField(verbose_name=_('authentication'), max_length=32, db_column='acctauthentic', null=True, blank=True) connection_info_start = models.CharField(verbose_name=_('connection info start'), max_length=50, db_column='connectinfo_start', null=True, blank=True) connection_info_stop = models.CharField(verbose_name=_('connection info stop'), max_length=50, db_column='connectinfo_stop', null=True, blank=True) input_octets = models.BigIntegerField(verbose_name=_('input octets'), db_column='acctinputoctets', null=True, blank=True) output_octets = models.BigIntegerField(verbose_name=_('output octets'), db_column='acctoutputoctets', null=True, blank=True) called_station_id = models.CharField(verbose_name=_('called station ID'), max_length=50, db_column='calledstationid', db_index=True, blank=True, null=True) calling_station_id = models.CharField(verbose_name=_('calling station ID'), max_length=50, db_column='callingstationid', db_index=True, blank=True, null=True) terminate_cause = models.CharField(verbose_name=_('termination cause'), max_length=32, db_column='acctterminatecause', blank=True, null=True) service_type = models.CharField(verbose_name=_('service type'), max_length=32, db_column='servicetype', null=True, blank=True) framed_protocol = models.CharField(verbose_name=_('framed protocol'), max_length=32, db_column='framedprotocol', null=True, blank=True) framed_ip_address = models.GenericIPAddressField(verbose_name=_('framed IP address'), db_column='framedipaddress', # the default MySQL freeradius schema defines # this as NOT NULL but defaulting to empty string # but that wouldn't work on PostgreSQL null=True, blank=True) framed_ipv6_address = models.GenericIPAddressField(verbose_name=_('framed IPv6 address'), db_column='framedipv6address', protocol='IPv6', null=True, blank=True) framed_ipv6_prefix = models.CharField(verbose_name=_('framed IPv6 prefix'), max_length=44, db_column='framedipv6prefix', validators=[ipv6_network_validator], null=True, blank=True) framed_interface_id = models.CharField(verbose_name=_('framed interface ID'), max_length=19, db_column='framedinterfaceid', null=True, blank=True) delegated_ipv6_prefix = models.CharField(verbose_name=_('delegated IPv6 prefix'), max_length=44, db_column='delegatedipv6prefix', validators=[ipv6_network_validator], null=True, blank=True) def save(self, *args, **kwargs): if not self.start_time: self.start_time = now() super(AbstractRadiusAccounting, self).save(*args, **kwargs) class Meta: db_table = 'radacct' verbose_name = _('accounting') verbose_name_plural = _('accountings') abstract = True def __str__(self): return self.unique_id class AbstractNas(BaseModel): name = models.CharField(verbose_name=_('name'), max_length=128, help_text=_('NAS Name (or IP address)'), db_index=True, db_column='nasname') short_name = models.CharField(verbose_name=_('short name'), max_length=32, db_column='shortname') type = models.CharField(verbose_name=_('type'), max_length=30, default='other', choices=RAD_NAS_TYPES) ports = models.PositiveIntegerField(verbose_name=_('ports'), blank=True, null=True) secret = models.CharField(verbose_name=_('secret'), max_length=60, help_text=_('Shared Secret')) server = models.CharField(verbose_name=_('server'), max_length=64, blank=True, null=True) community = models.CharField(verbose_name=_('community'), max_length=50, blank=True, null=True) description = models.CharField(verbose_name=_('description'), max_length=200, null=True, blank=True) class Meta: db_table = 'nas' verbose_name = _('NAS') verbose_name_plural = _('NAS') abstract = True def __str__(self): return self.name class AbstractRadiusGroup(BaseModel): """ This is not part of the standard freeradius schema. It's added to facilitate the management of groups. """ id = TimeStampedEditableModel._meta.get_field('id') name = models.CharField(verbose_name=_('group name'), max_length=255, unique=True, db_index=True) description = models.CharField(verbose_name=_('description'), max_length=64, blank=True, null=True) _DEFAULT_HELP_TEXT = ( 'The default group is automatically assigned to new users; ' 'changing the default group has only effect on new users ' '(existing users will keep being members of their current group)' ) default = models.BooleanField(verbose_name=_('is default?'), help_text=_(_DEFAULT_HELP_TEXT), default=False) class Meta: verbose_name = _('group') verbose_name_plural = _('groups') abstract = True def __str__(self): return self.name def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._initial_default = self.default def clean(self): self.check_default() def save(self, *args, **kwargs): result = super().save(*args, **kwargs) if self.default: self.set_default() # sync all related records if not self._state.adding: self.radiusgroupcheck_set.update(groupname=self.name) self.radiusgroupreply_set.update(groupname=self.name) self.radiususergroup_set.update(groupname=self.name) return result _DEFAULT_VALIDATION_ERROR = _( 'There must be at least one default group present in ' 'the system. To change the default group, simply set ' 'as deafult the group you want to make the new deafult.' ) _DEFAULT_PROTECTED_ERROR = _('The default group cannot be deleted') def delete(self, *args, **kwargs): if self.default: raise ProtectedError(self._DEFAULT_PROTECTED_ERROR, self) return super().delete(*args, **kwargs) def set_default(self): """ ensures there's only 1 default group (logic overridable via custom models) """ queryset = self.get_default_queryset() if queryset.exists(): queryset.update(default=False) def check_default(self): """ ensures the default group cannot be undefaulted (logic overridable via custom models) """ if not self.default and self._initial_default: raise ValidationError( {'default': self._DEFAULT_VALIDATION_ERROR} ) def get_default_queryset(self): """ looks for default groups excluding the current one overridable by openwisp-radius and other 3rd party apps """ return self.__class__.objects.exclude(pk=self.pk) \ .filter(default=True) class AbstractRadiusUserGroup(AutoGroupnameMixin, AutoUsernameMixin, BaseModel): username = models.CharField(verbose_name=_('username'), max_length=64, db_index=True, # blank values are forbidden with custom validation # because this field can left blank if the user # foreign key is filled (it will be auto-filled) blank=True) groupname = models.CharField(verbose_name=_('group name'), max_length=64, # blank values are forbidden with custom validation # because this field can left blank if the group # foreign key is filled (it will be auto-filled) blank=True) priority = models.IntegerField(verbose_name=_('priority'), default=1) # the foreign keys are not part of the standard freeradius schema, # these are added here to facilitate the synchronization of the # records which are related in different tables user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, blank=True, null=True) group = models.ForeignKey('RadiusGroup', on_delete=models.CASCADE, blank=True, null=True) class Meta: db_table = 'radusergroup' verbose_name = _('user group') verbose_name_plural = _('user groups') abstract = True def __str__(self): return str(self.username) class AbstractRadiusGroupCheck(AutoGroupnameMixin, BaseModel): groupname = models.CharField(verbose_name=_('group name'), max_length=64, db_index=True, # blank values are forbidden with custom validation # because this field can left blank if the group # foreign key is filled (it will be auto-filled) blank=True) attribute = models.CharField(verbose_name=_('attribute'), max_length=64) op = models.CharField(verbose_name=_('operator'), max_length=2, choices=RADOP_CHECK_TYPES, default=':=') value = models.CharField(verbose_name=_('value'), max_length=253) # the foreign key is not part of the standard freeradius schema group = models.ForeignKey('RadiusGroup', on_delete=models.CASCADE, blank=True, null=True) class Meta: db_table = 'radgroupcheck' verbose_name = _('group check') verbose_name_plural = _('group checks') abstract = True def __str__(self): return str(self.groupname) class AbstractRadiusGroupReply(AutoGroupnameMixin, BaseModel): groupname = models.CharField(verbose_name=_('group name'), max_length=64, db_index=True, # blank values are forbidden with custom validation # because this field can left blank if the group # foreign key is filled (it will be auto-filled) blank=True) attribute = models.CharField(verbose_name=_('attribute'), max_length=64) op = models.CharField(verbose_name=_('operator'), max_length=2, choices=RADOP_REPLY_TYPES, default='=') value = models.CharField(verbose_name=_('value'), max_length=253) # the foreign key is not part of the standard freeradius schema group = models.ForeignKey('RadiusGroup', on_delete=models.CASCADE, blank=True, null=True) class Meta: db_table = 'radgroupreply' verbose_name = _('group reply') verbose_name_plural = _('group replies') abstract = True def __str__(self): return str(self.groupname) class AbstractRadiusPostAuth(models.Model): username = models.CharField(verbose_name=_('username'), max_length=64) password = models.CharField(verbose_name=_('password'), max_length=64, db_column='pass', blank=True) reply = models.CharField(verbose_name=_('reply'), max_length=32) called_station_id = models.CharField(verbose_name=_('called station ID'), max_length=50, db_column='calledstationid', blank=True, null=True) calling_station_id = models.CharField(verbose_name=_('calling station ID'), max_length=50, db_column='callingstationid', blank=True, null=True) date = models.DateTimeField(verbose_name=_('date'), db_column='authdate', auto_now_add=True) class Meta: db_table = 'radpostauth' verbose_name = _('post auth') verbose_name_plural = _('post auth log') abstract = True def __str__(self): return str(self.username) class AbstractRadiusBatch(TimeStampedEditableModel): strategy = models.CharField(_('strategy'), max_length=16, choices=STRATEGIES, db_index=True, help_text=_('Import users from a CSV or generate using a prefix')) name = models.CharField(verbose_name=_('name'), max_length=128, help_text=_('A unique batch name'), db_index=True, unique=True) users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, related_name='radius_batch', help_text=_('List of users uploaded in this batch')) csvfile = models.FileField(null=True, blank=True, verbose_name='CSV', help_text=_('The csv file containing the user details to be uploaded')) prefix = models.CharField(_('prefix'), null=True, blank=True, max_length=20, help_text=_('Usernames generated will be of the format [prefix][number]')) pdf = models.FileField(null=True, blank=True, verbose_name='PDF', help_text=_('The pdf file containing list of usernames and passwords')) expiration_date = models.DateField(verbose_name=_('expiration date'), null=True, blank=True, help_text=_('If left blank users will never expire')) class Meta: db_table = 'radbatch' verbose_name = _('batch user creation') verbose_name_plural = _('batch user creation operations') abstract = True def __str__(self): return self.name def clean(self): if self.strategy == 'csv' and not self.csvfile: raise ValidationError({'csvfile': _('This field cannot be blank.')}, code='invalid') if self.strategy == 'prefix' and not self.prefix: raise ValidationError({'prefix': _('This field cannot be blank.')}, code='invalid') if self.strategy == 'csv' and self.prefix or self.strategy == 'prefix' and self.csvfile: # this case would happen only when using the internal API raise ValidationError( _('Mixing fields of different strategies'), code='invalid', ) if self.strategy == 'csv': validate_csvfile(self.csvfile.file) super(AbstractRadiusBatch, self).clean() def add(self, reader, password_length=BATCH_DEFAULT_PASSWORD_LENGTH): users_list = [] generated_passwords = [] for row in reader: if len(row) == 5: user, password = self.get_or_create_user(row, users_list, password_length) users_list.append(user) if password: generated_passwords.append(password) for user in users_list: self.save_user(user) for element in generated_passwords: username, password, user_email = element send_mail( BATCH_MAIL_SUBJECT, BATCH_MAIL_MESSAGE.format(username, password), BATCH_MAIL_SENDER, [user_email] ) def csvfile_upload(self, csvfile, password_length=BATCH_DEFAULT_PASSWORD_LENGTH): csv_data = csvfile.read() csv_data = csv_data.decode('utf-8') if isinstance(csv_data, bytes) else csv_data reader = csv.reader(StringIO(csv_data), delimiter=',') self.full_clean() self.save() self.add(reader, password_length) def prefix_add(self, prefix, n, password_length=BATCH_DEFAULT_PASSWORD_LENGTH): self.save() users_list, user_password = prefix_generate_users(prefix, n, password_length) for user in users_list: user.full_clean() self.save_user(user) pdf_file = generate_pdf(prefix, {'users': user_password}) pdf_file.name = pdf_file.name.split('/')[-1] self.pdf = pdf_file self.full_clean() self.save() def get_or_create_user(self, row, users_list, password_length): generated_password = None username, password, email, first_name, last_name = row if not username and email: username = email.split('@')[0] username = find_available_username(username, users_list) user = User(username=username, email=email, first_name=first_name, last_name=last_name) cleartext_delimiter = 'cleartext$' if not password: password = get_random_string(length=password_length) user.set_password(password) generated_password = ([username, password, email]) elif password and password.startswith(cleartext_delimiter): password = password[len(cleartext_delimiter):] user.set_password(password) else: user.password = password user.full_clean() return user, generated_password def save_user(self, user): user.save() self.users.add(user) def delete(self): self.users.all().delete() super().delete() self._remove_files() def expire(self): users = self.users.all() for u in users: u.is_active = False u.save() def _remove_files(self): strategy_filemap = { 'prefix': 'pdf', 'csv': 'csvfile' } path = getattr(self, strategy_filemap.get(self.strategy)).path if os.path.isfile(path): os.remove(path) class AbstractRadiusToken(TimeStampedEditableModel, models.Model): # key field is a primary key so additional id field will be redundant id = None # tokens are not supposed to be modified, can be regenerated if necessary modified = None key = models.CharField(_('Key'), max_length=40, primary_key=True) user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='radius_token') class Meta: db_table = 'radiustoken' verbose_name = _('radius token') verbose_name_plural = _('radius token') abstract = True def save(self, *args, **kwargs): if not self.key: self.key = self.generate_key() return super().save(*args, **kwargs) def generate_key(self): return get_random_string(length=40) def __str__(self): return self.key