diff --git a/.gitignore b/.gitignore index 33e22ca..b2fe60b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ target/ # editors *.komodoproject +.vscode # other *.DS_Store* diff --git a/.travis.yml b/.travis.yml index f199288..d1f093e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ before_install: install: - pip install $DJANGO - python setup.py -q develop + - pip install --upgrade https://github.com/openwisp/openwisp-utils/tarball/master#egg=openwisp_utils[qa] before_script: - ./run-qa-checks diff --git a/README.rst b/README.rst index 8f9118a..2190ce5 100644 --- a/README.rst +++ b/README.rst @@ -3,22 +3,34 @@ django-x509 .. image:: https://travis-ci.org/openwisp/django-x509.svg :target: https://travis-ci.org/openwisp/django-x509 - :alt: Automated Test Build + :alt: CI build status .. image:: https://coveralls.io/repos/openwisp/django-x509/badge.svg :target: https://coveralls.io/r/openwisp/django-x509 - :alt: Coverage + :alt: Test Coverage .. image:: https://requires.io/github/openwisp/django-x509/requirements.svg?branch=master :target: https://requires.io/github/openwisp/django-x509/requirements/?branch=master :alt: Requirements Status +.. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg + :target: https://gitter.im/openwisp/general + :alt: chat + .. image:: https://badge.fury.io/py/django-x509.svg :target: http://badge.fury.io/py/django-x509 :alt: Pypi Version -.. image:: https://github.com/openwisp/django-x509/tree/master/docs/demo_x509.gif +.. image:: https://pepy.tech/badge/django-x509 + :target: https://pepy.tech/project/django-x509 + :alt: downloads + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://pypi.org/project/black/ + :alt: code style: black +.. image:: https://github.com/openwisp/django-x509/raw/master/docs/demo_x509.gif + :alt: demo ------------ Simple reusable django app implementing x509 PKI certificates management. @@ -612,21 +624,12 @@ For more information about automated tests in django, please refer to Contributing ------------ -Please read the `OpenWISP contributing guidelines -`_ -and also keep in mind the following: +Please refer to the `OpenWISP contributing guidelines `_. -1. Announce your intentions in the `OpenWISP Mailing List `_ -2. Fork this repo and install it -3. Follow `PEP8, Style Guide for Python Code`_ -4. Write code -5. Write tests for your code -6. Ensure all tests pass -7. Ensure test coverage does not decrease -8. Document your changes -9. Send pull request +Support +------- -.. _PEP8, Style Guide for Python Code: http://www.python.org/dev/peps/pep-0008/ +See `OpenWISP Support Channels `_. Changelog --------- @@ -637,8 +640,3 @@ License ------- See `LICENSE `_. - -Support -------- - -See `OpenWISP Support Channels `_. diff --git a/django_x509/base/admin.py b/django_x509/base/admin.py index b7c5252..1559047 100644 --- a/django_x509/base/admin.py +++ b/django_x509/base/admin.py @@ -16,7 +16,7 @@ class X509Form(forms.ModelForm): OPERATION_CHOICES = ( ('-', '----- {0} -----'.format(_('Please select an option'))), ('new', _('Create new')), - ('import', _('Import Existing')) + ('import', _('Import Existing')), ) operation_type = forms.ChoiceField(choices=OPERATION_CHOICES) @@ -25,30 +25,29 @@ class BaseAdmin(ModelAdmin): """ ModelAdmin for TimeStampedEditableModel """ - list_display = ['name', - 'key_length', - 'digest', - 'created', - 'modified'] + + list_display = ['name', 'key_length', 'digest', 'created', 'modified'] search_fields = ['name', 'serial_number', 'common_name'] actions_on_bottom = True save_on_top = True form = X509Form # custom attribute - readonly_edit = ['key_length', - 'digest', - 'validity_start', - 'validity_end', - 'country_code', - 'state', - 'city', - 'organization_name', - 'organizational_unit_name', - 'email', - 'common_name', - 'serial_number', - 'certificate', - 'private_key'] + readonly_edit = [ + 'key_length', + 'digest', + 'validity_start', + 'validity_end', + 'country_code', + 'state', + 'city', + 'organization_name', + 'organizational_unit_name', + 'email', + 'common_name', + 'serial_number', + 'certificate', + 'private_key', + ] class Media: css = {'all': (static('django-x509/css/admin.css'),)} @@ -77,55 +76,57 @@ def get_fields(self, request, obj=None): def get_context(self, data, ca_count=0, cert_count=0): context = dict() if ca_count: - context.update({ - 'title': _('Renew selected CAs'), - 'ca_count': ca_count, - 'cert_count': cert_count, - 'cancel_url': f'{self.opts.app_label}_ca_changelist', - 'action': 'renew_ca' - }) + context.update( + { + 'title': _('Renew selected CAs'), + 'ca_count': ca_count, + 'cert_count': cert_count, + 'cancel_url': f'{self.opts.app_label}_ca_changelist', + 'action': 'renew_ca', + } + ) else: - context.update({ - 'title': _('Renew selected certs'), - 'cert_count': cert_count, - 'cancel_url': f'{self.opts.app_label}_cert_changelist', - 'action': 'renew_cert' - }) - context.update({ - 'opts': self.model._meta, - 'data': data - }) + context.update( + { + 'title': _('Renew selected certs'), + 'cert_count': cert_count, + 'cancel_url': f'{self.opts.app_label}_cert_changelist', + 'action': 'renew_cert', + } + ) + context.update({'opts': self.model._meta, 'data': data}) return context class AbstractCaAdmin(BaseAdmin): list_filter = ['key_length', 'digest', 'created'] - fields = ['operation_type', - 'name', - 'notes', - 'key_length', - 'digest', - 'validity_start', - 'validity_end', - 'country_code', - 'state', - 'city', - 'organization_name', - 'organizational_unit_name', - 'email', - 'common_name', - 'extensions', - 'serial_number', - 'certificate', - 'private_key', - 'passphrase', - 'created', - 'modified'] + fields = [ + 'operation_type', + 'name', + 'notes', + 'key_length', + 'digest', + 'validity_start', + 'validity_end', + 'country_code', + 'state', + 'city', + 'organization_name', + 'organizational_unit_name', + 'email', + 'common_name', + 'extensions', + 'serial_number', + 'certificate', + 'private_key', + 'passphrase', + 'created', + 'modified', + ] actions = ['renew_ca'] class Media: - js = ('admin/js/jquery.init.js', - 'django-x509/js/x509-admin.js',) + js = ('admin/js/jquery.init.js', 'django-x509/js/x509-admin.js') def get_urls(self): return [ @@ -136,13 +137,11 @@ def crl_view(self, request, pk): authenticated = request.user.is_authenticated authenticated = authenticated() if callable(authenticated) else authenticated if app_settings.CRL_PROTECTED and not authenticated: - return HttpResponse(_('Forbidden'), - status=403, - content_type='text/plain') + return HttpResponse(_('Forbidden'), status=403, content_type='text/plain') instance = get_object_or_404(self.model, pk=pk) - return HttpResponse(instance.crl, - status=200, - content_type='application/x-pem-file') + return HttpResponse( + instance.crl, status=200, content_type='application/x-pem-file' + ) def renew_ca(self, request, queryset): if request.POST.get('post'): @@ -151,11 +150,16 @@ def renew_ca(self, request, queryset): ca.renew() renewed_rows += 1 message = ngettext( - '%(renewed_rows)d CA and its related certificates have been successfully renewed', - '%(renewed_rows)d CAs and their related certificates have been successfully renewed', - renewed_rows) % { - 'renewed_rows': renewed_rows - } + ( + '%(renewed_rows)d CA and its related certificates have ' + 'been successfully renewed' + ), + ( + '%(renewed_rows)d CAs and their related ' + 'certificates have been successfully renewed' + ), + renewed_rows, + ) % {'renewed_rows': renewed_rows} self.message_user(request, message) else: data = dict() @@ -169,7 +173,9 @@ def renew_ca(self, request, queryset): return render( request, 'admin/django_x509/renew_confirmation.html', - context=self.get_context(data, ca_count=ca_count, cert_count=cert_count) + context=self.get_context( + data, ca_count=ca_count, cert_count=cert_count + ), ) renew_ca.short_description = _('Renew selected CAs') @@ -179,41 +185,43 @@ class AbstractCertAdmin(BaseAdmin): list_filter = ['ca', 'revoked', 'key_length', 'digest', 'created'] list_select_related = ['ca'] readonly_fields = ['revoked', 'revoked_at'] - fields = ['operation_type', - 'name', - 'ca', - 'notes', - 'revoked', - 'revoked_at', - 'key_length', - 'digest', - 'validity_start', - 'validity_end', - 'country_code', - 'state', - 'city', - 'organization_name', - 'organizational_unit_name', - 'email', - 'common_name', - 'extensions', - 'serial_number', - 'certificate', - 'private_key', - 'passphrase', - 'created', - 'modified'] + fields = [ + 'operation_type', + 'name', + 'ca', + 'notes', + 'revoked', + 'revoked_at', + 'key_length', + 'digest', + 'validity_start', + 'validity_end', + 'country_code', + 'state', + 'city', + 'organization_name', + 'organizational_unit_name', + 'email', + 'common_name', + 'extensions', + 'serial_number', + 'certificate', + 'private_key', + 'passphrase', + 'created', + 'modified', + ] actions = ['revoke_action', 'renew_cert'] class Media: - js = ('admin/js/jquery.init.js', - 'django-x509/js/x509-admin.js',) + js = ('admin/js/jquery.init.js', 'django-x509/js/x509-admin.js') def ca_url(self, obj): - url = reverse('admin:{0}_ca_change'.format(self.opts.app_label), args=[obj.ca.pk]) - return format_html('{text}', - url=url, - text=obj.ca.name) + url = reverse( + 'admin:{0}_ca_change'.format(self.opts.app_label), args=[obj.ca.pk] + ) + return format_html('{text}', url=url, text=obj.ca.name) + ca_url.short_description = 'CA' def revoke_action(self, request, queryset): @@ -239,18 +247,17 @@ def renew_cert(self, request, queryset): message = ngettext( '%(renewed_rows)d Certificate has been successfully renewed', '%(renewed_rows)d Certificates have been successfully renewed', - renewed_rows) % { - 'renewed_rows': renewed_rows - } + renewed_rows, + ) % {'renewed_rows': renewed_rows} self.message_user(request, message) else: return render( request, 'admin/django_x509/renew_confirmation.html', - context=self.get_context(queryset, cert_count=len(queryset)) + context=self.get_context(queryset, cert_count=len(queryset)), ) - renew_cert.short_description = _("Renew selected certificates") + renew_cert.short_description = _('Renew selected certificates') # For backward compatibility diff --git a/django_x509/base/models.py b/django_x509/base/models.py index e3dc2d7..9212c9c 100644 --- a/django_x509/base/models.py +++ b/django_x509/base/models.py @@ -24,7 +24,7 @@ ('512', '512'), ('1024', '1024'), ('2048', '2048'), - ('4096', '4096') + ('4096', '4096'), ) DIGEST_CHOICES = ( @@ -47,8 +47,9 @@ def datetime_to_string(datetime_): """ - Converts datetime.datetime object to UTCTime/GeneralizedTime string following RFC5280. - (Returns string encoded in UTCtime for dates through year 2049, otherwise in GeneralizedTime format) + Converts datetime.datetime object to UTCTime/GeneralizedTime string + following RFC5280. (Returns string encoded in UTCtime for dates through year + 2049, otherwise in GeneralizedTime format) """ if datetime_.year < 2050: return datetime_.strftime(utc_time) @@ -107,54 +108,70 @@ class BaseX509(models.Model): """ Abstract Cert class, shared between Ca and Cert """ + name = models.CharField(max_length=64) notes = models.TextField(blank=True) - key_length = models.CharField(_('key length'), - help_text=_('bits'), - blank=True, - choices=KEY_LENGTH_CHOICES, - default=default_key_length, - max_length=6) - digest = models.CharField(_('digest algorithm'), - help_text=_('bits'), - blank=True, - choices=DIGEST_CHOICES, - default=default_digest_algorithm, - max_length=8) - validity_start = models.DateTimeField(blank=True, - null=True, - default=default_validity_start) - validity_end = models.DateTimeField(blank=True, - null=True, - default=default_cert_validity_end) + key_length = models.CharField( + _('key length'), + help_text=_('bits'), + blank=True, + choices=KEY_LENGTH_CHOICES, + default=default_key_length, + max_length=6, + ) + digest = models.CharField( + _('digest algorithm'), + help_text=_('bits'), + blank=True, + choices=DIGEST_CHOICES, + default=default_digest_algorithm, + max_length=8, + ) + validity_start = models.DateTimeField( + blank=True, null=True, default=default_validity_start + ) + validity_end = models.DateTimeField( + blank=True, null=True, default=default_cert_validity_end + ) country_code = models.CharField(max_length=2, blank=True) state = models.CharField(_('state or province'), max_length=64, blank=True) city = models.CharField(_('city'), max_length=64, blank=True) organization_name = models.CharField(_('organization'), max_length=64, blank=True) - organizational_unit_name = models.CharField(_('organizational unit name'), - max_length=64, blank=True) + organizational_unit_name = models.CharField( + _('organizational unit name'), max_length=64, blank=True + ) email = models.EmailField(_('email address'), blank=True) common_name = models.CharField(_('common name'), max_length=63, blank=True) - extensions = JSONField(_('extensions'), - default=list, - blank=True, - help_text=_('additional x509 certificate extensions'), - load_kwargs={'object_pairs_hook': collections.OrderedDict}, - dump_kwargs={'indent': 4}) + extensions = JSONField( + _('extensions'), + default=list, + blank=True, + help_text=_('additional x509 certificate extensions'), + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + dump_kwargs={'indent': 4}, + ) # serial_number is set to CharField as a UUID integer is too big for a # PositiveIntegerField and an IntegerField on SQLite - serial_number = models.CharField(_('serial number'), - help_text=_('leave blank to determine automatically'), - blank=True, - null=True, - max_length=48) - certificate = models.TextField(blank=True, help_text='certificate in X.509 PEM format') - private_key = models.TextField(blank=True, help_text='private key in X.509 PEM format') + serial_number = models.CharField( + _('serial number'), + help_text=_('leave blank to determine automatically'), + blank=True, + null=True, + max_length=48, + ) + certificate = models.TextField( + blank=True, help_text='certificate in X.509 PEM format' + ) + private_key = models.TextField( + blank=True, help_text='private key in X.509 PEM format' + ) created = AutoCreatedField(_('created'), editable=True) modified = AutoLastModifiedField(_('modified'), editable=True) - passphrase = models.CharField(max_length=64, - blank=True, - help_text=_('Passphrase for the private key, if present')) + passphrase = models.CharField( + max_length=64, + blank=True, + help_text=_('Passphrase for the private key, if present'), + ) class Meta: abstract = True @@ -173,12 +190,15 @@ def clean_fields(self, *args, **kwargs): def clean(self): # when importing, both public and private must be present - if ( - (self.certificate and not self.private_key) or - (self.private_key and not self.certificate) + if (self.certificate and not self.private_key) or ( + self.private_key and not self.certificate ): - raise ValidationError(_('When importing an existing certificate, both' - 'keys (private and public) must be present')) + raise ValidationError( + _( + 'When importing an existing certificate, both' + 'keys (private and public) must be present' + ) + ) if self.serial_number: self._validate_serial_number() self._verify_extension_format() @@ -220,9 +240,11 @@ def pkey(self): returns an instance of OpenSSL.crypto.PKey """ if self.private_key: - return crypto.load_privatekey(crypto.FILETYPE_PEM, - self.private_key, - passphrase=getattr(self, 'passphrase').encode('utf-8')) + return crypto.load_privatekey( + crypto.FILETYPE_PEM, + self.private_key, + passphrase=getattr(self, 'passphrase').encode('utf-8'), + ) def _validate_pem(self): """ @@ -240,7 +262,9 @@ def _validate_pem(self): kwargs['passphrase'] = getattr(self, 'passphrase').encode('utf8') load_pem(*args, **kwargs) except OpenSSL.crypto.Error as e: - error = 'OpenSSL error:
{0}'.format(str(e.args[0]).replace('), ', '),
').strip('[]')) + error = 'OpenSSL error:
{0}'.format( + str(e.args[0]).replace('), ', '),
').strip('[]') + ) if 'bad decrypt' in error: error = 'Incorrect Passphrase
' + error errors['passphrase'] = ValidationError(_(mark_safe(error))) @@ -257,7 +281,9 @@ def _validate_serial_number(self): try: int(self.serial_number) except ValueError: - raise ValidationError({'serial_number': _('Serial number must be an integer')}) + raise ValidationError( + {'serial_number': _('Serial number must be an integer')} + ) def _generate(self): """ @@ -285,13 +311,17 @@ def _generate(self): cert.set_pubkey(key) cert = self._add_extensions(cert) cert.sign(issuer_key, str(self.digest)) - self.certificate = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode('utf-8') + self.certificate = crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode( + 'utf-8' + ) key_args = (crypto.FILETYPE_PEM, key) key_kwargs = {} if self.passphrase: key_kwargs['passphrase'] = self.passphrase.encode('utf-8') key_kwargs['cipher'] = 'DES-EDE3-CBC' - self.private_key = crypto.dump_privatekey(*key_args, **key_kwargs).decode('utf-8') + self.private_key = crypto.dump_privatekey(*key_args, **key_kwargs).decode( + 'utf-8' + ) def _fill_subject(self, subject): """ @@ -305,7 +335,7 @@ def _fill_subject(self, subject): 'organization_name': 'organizationName', 'organizational_unit_name': 'organizationalUnitName', 'email': 'emailAddress', - 'common_name': 'commonName' + 'common_name': 'commonName', } # set x509 subject attributes only if not empty strings for model_attr, subject_attr in attr_map.items(): @@ -364,8 +394,10 @@ def _verify_ca(self): try: store_ctx.verify_certificate() except crypto.X509StoreContextError as e: - raise ValidationError(_("CA doesn't match, got the " - "following error from pyOpenSSL: \"%s\"") % e.args[0][2]) + raise ValidationError( + _("CA doesn't match, got the " 'following error from pyOpenSSL: "%s"') + % e.args[0][2] + ) def _verify_extension_format(self): """ @@ -393,41 +425,58 @@ def _add_extensions(self, cert): ext_value = 'CA:TRUE' if pathlen is not None: ext_value = '{0}, pathlen:{1}'.format(ext_value, pathlen) - ext.append(crypto.X509Extension(b'basicConstraints', - app_settings.CA_BASIC_CONSTRAINTS_CRITICAL, - bytes(str(ext_value), 'utf8'))) - ext.append(crypto.X509Extension(b'keyUsage', - app_settings.CA_KEYUSAGE_CRITICAL, - bytes(str(app_settings.CA_KEYUSAGE_VALUE), 'utf8'))) + ext.append( + crypto.X509Extension( + b'basicConstraints', + app_settings.CA_BASIC_CONSTRAINTS_CRITICAL, + bytes(str(ext_value), 'utf8'), + ) + ) + ext.append( + crypto.X509Extension( + b'keyUsage', + app_settings.CA_KEYUSAGE_CRITICAL, + bytes(str(app_settings.CA_KEYUSAGE_VALUE), 'utf8'), + ) + ) issuer_cert = cert # prepare extensions for end-entity certs else: - ext.append(crypto.X509Extension(b'basicConstraints', - False, - b'CA:FALSE')) - ext.append(crypto.X509Extension(b'keyUsage', - app_settings.CERT_KEYUSAGE_CRITICAL, - bytes(str(app_settings.CERT_KEYUSAGE_VALUE), 'utf8'))) + ext.append(crypto.X509Extension(b'basicConstraints', False, b'CA:FALSE')) + ext.append( + crypto.X509Extension( + b'keyUsage', + app_settings.CERT_KEYUSAGE_CRITICAL, + bytes(str(app_settings.CERT_KEYUSAGE_VALUE), 'utf8'), + ) + ) issuer_cert = self.ca.x509 - ext.append(crypto.X509Extension(b'subjectKeyIdentifier', - False, - b'hash', - subject=cert)) + ext.append( + crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=cert) + ) cert.add_extensions(ext) # authorityKeyIdentifier must be added after # the other extensions have been already added - cert.add_extensions([ - crypto.X509Extension(b'authorityKeyIdentifier', - False, - b'keyid:always,issuer:always', - issuer=issuer_cert) - ]) + cert.add_extensions( + [ + crypto.X509Extension( + b'authorityKeyIdentifier', + False, + b'keyid:always,issuer:always', + issuer=issuer_cert, + ) + ] + ) for ext in self.extensions: - cert.add_extensions([ - crypto.X509Extension(bytes(str(ext['name']), 'utf8'), - bool(ext['critical']), - bytes(str(ext['value']), 'utf8')) - ]) + cert.add_extensions( + [ + crypto.X509Extension( + bytes(str(ext['name']), 'utf8'), + bool(ext['critical']), + bytes(str(ext['value']), 'utf8'), + ) + ] + ) return cert def renew(self): @@ -444,6 +493,7 @@ class AbstractCa(BaseX509): """ Abstract Ca model """ + class Meta: abstract = True verbose_name = _('CA') @@ -455,9 +505,9 @@ def get_revoked_certs(self): (does not include expired certificates) """ now = timezone.now() - return self.cert_set.filter(revoked=True, - validity_start__lte=now, - validity_end__gte=now) + return self.cert_set.filter( + revoked=True, validity_start__lte=now, validity_end__gte=now + ) def renew(self): """ @@ -492,15 +542,16 @@ class AbstractCert(BaseX509): """ Abstract Cert model """ - ca = models.ForeignKey(swapper.get_model_name('django_x509', 'Ca'), - on_delete=models.CASCADE, - verbose_name=_('CA')) - revoked = models.BooleanField(_('revoked'), - default=False) - revoked_at = models.DateTimeField(_('revoked at'), - blank=True, - null=True, - default=None) + + ca = models.ForeignKey( + swapper.get_model_name('django_x509', 'Ca'), + on_delete=models.CASCADE, + verbose_name=_('CA'), + ) + revoked = models.BooleanField(_('revoked'), default=False) + revoked_at = models.DateTimeField( + _('revoked at'), blank=True, null=True, default=None + ) def __str__(self): return self.name diff --git a/django_x509/migrations/0001_initial.py b/django_x509/migrations/0001_initial.py index a518463..41cdce9 100644 --- a/django_x509/migrations/0001_initial.py +++ b/django_x509/migrations/0001_initial.py @@ -14,63 +14,311 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( name='Ca', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), - ('key_length', models.CharField(blank=True, choices=[(b'', b''), (b'512', b'512'), (b'1024', b'1024'), (b'2048', b'2048'), (b'4096', b'4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), - ('digest', models.CharField(blank=True, choices=[(b'', b''), (b'sha1', b'SHA1'), (b'sha224', b'SHA224'), (b'sha256', b'SHA256'), (b'sha384', b'SHA384'), (b'sha512', b'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), - ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), - ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_ca_validity_end, null=True)), + ( + 'key_length', + models.CharField( + blank=True, + choices=[ + (b'', b''), + (b'512', b'512'), + (b'1024', b'1024'), + (b'2048', b'2048'), + (b'4096', b'4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), + ), + ( + 'digest', + models.CharField( + blank=True, + choices=[ + (b'', b''), + (b'sha1', b'SHA1'), + (b'sha224', b'SHA224'), + (b'sha256', b'SHA256'), + (b'sha384', b'SHA384'), + (b'sha512', b'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), + ), + ( + 'validity_start', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_validity_start, + null=True, + ), + ), + ( + 'validity_end', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_ca_validity_end, + null=True, + ), + ), ('country_code', models.CharField(blank=True, max_length=2)), - ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), - ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), - ('organization', models.CharField(blank=True, max_length=64, verbose_name='organization')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), - ('extensions', jsonfield.fields.JSONField(blank=True, default=list, dump_kwargs={'indent': 4}, help_text='additional x509 certificate extensions', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='extensions')), - ('serial_number', models.PositiveIntegerField(blank=True, help_text='leave blank to determine automatically', null=True, verbose_name='serial number')), - ('public_key', models.TextField(blank=True, help_text=b'certificate in X.509 PEM format')), - ('private_key', models.TextField(blank=True, help_text=b'private key in X.509 PEM format')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ( + 'state', + models.CharField( + blank=True, max_length=64, verbose_name='state or province' + ), + ), + ( + 'city', + models.CharField(blank=True, max_length=64, verbose_name='city'), + ), + ( + 'organization', + models.CharField( + blank=True, max_length=64, verbose_name='organization' + ), + ), + ( + 'email', + models.EmailField( + blank=True, max_length=254, verbose_name='email address' + ), + ), + ( + 'common_name', + models.CharField( + blank=True, max_length=63, verbose_name='common name' + ), + ), + ( + 'extensions', + jsonfield.fields.JSONField( + blank=True, + default=list, + dump_kwargs={'indent': 4}, + help_text='additional x509 certificate extensions', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='extensions', + ), + ), + ( + 'serial_number', + models.PositiveIntegerField( + blank=True, + help_text='leave blank to determine automatically', + null=True, + verbose_name='serial number', + ), + ), + ( + 'public_key', + models.TextField( + blank=True, help_text=b'certificate in X.509 PEM format' + ), + ), + ( + 'private_key', + models.TextField( + blank=True, help_text=b'private key in X.509 PEM format' + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), ], - options={ - 'verbose_name': 'CA', - 'verbose_name_plural': 'CAs', - }, + options={'verbose_name': 'CA', 'verbose_name_plural': 'CAs'}, ), migrations.CreateModel( name='Cert', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), - ('key_length', models.CharField(blank=True, choices=[(b'', b''), (b'512', b'512'), (b'1024', b'1024'), (b'2048', b'2048'), (b'4096', b'4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), - ('digest', models.CharField(blank=True, choices=[(b'', b''), (b'sha1', b'SHA1'), (b'sha224', b'SHA224'), (b'sha256', b'SHA256'), (b'sha384', b'SHA384'), (b'sha512', b'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), - ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), - ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_cert_validity_end, null=True)), + ( + 'key_length', + models.CharField( + blank=True, + choices=[ + (b'', b''), + (b'512', b'512'), + (b'1024', b'1024'), + (b'2048', b'2048'), + (b'4096', b'4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), + ), + ( + 'digest', + models.CharField( + blank=True, + choices=[ + (b'', b''), + (b'sha1', b'SHA1'), + (b'sha224', b'SHA224'), + (b'sha256', b'SHA256'), + (b'sha384', b'SHA384'), + (b'sha512', b'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), + ), + ( + 'validity_start', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_validity_start, + null=True, + ), + ), + ( + 'validity_end', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_cert_validity_end, + null=True, + ), + ), ('country_code', models.CharField(blank=True, max_length=2)), - ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), - ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), - ('organization', models.CharField(blank=True, max_length=64, verbose_name='organization')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), - ('extensions', jsonfield.fields.JSONField(blank=True, default=list, dump_kwargs={'indent': 4}, help_text='additional x509 certificate extensions', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='extensions')), - ('serial_number', models.PositiveIntegerField(blank=True, help_text='leave blank to determine automatically', null=True, verbose_name='serial number')), - ('public_key', models.TextField(blank=True, help_text=b'certificate in X.509 PEM format')), - ('private_key', models.TextField(blank=True, help_text=b'private key in X.509 PEM format')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ( + 'state', + models.CharField( + blank=True, max_length=64, verbose_name='state or province' + ), + ), + ( + 'city', + models.CharField(blank=True, max_length=64, verbose_name='city'), + ), + ( + 'organization', + models.CharField( + blank=True, max_length=64, verbose_name='organization' + ), + ), + ( + 'email', + models.EmailField( + blank=True, max_length=254, verbose_name='email address' + ), + ), + ( + 'common_name', + models.CharField( + blank=True, max_length=63, verbose_name='common name' + ), + ), + ( + 'extensions', + jsonfield.fields.JSONField( + blank=True, + default=list, + dump_kwargs={'indent': 4}, + help_text='additional x509 certificate extensions', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='extensions', + ), + ), + ( + 'serial_number', + models.PositiveIntegerField( + blank=True, + help_text='leave blank to determine automatically', + null=True, + verbose_name='serial number', + ), + ), + ( + 'public_key', + models.TextField( + blank=True, help_text=b'certificate in X.509 PEM format' + ), + ), + ( + 'private_key', + models.TextField( + blank=True, help_text=b'private key in X.509 PEM format' + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), ('revoked', models.BooleanField(default=False, verbose_name='revoked')), - ('revoked_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='revoked at')), - ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_x509.Ca', verbose_name='CA')), + ( + 'revoked_at', + models.DateTimeField( + blank=True, default=None, null=True, verbose_name='revoked at' + ), + ), + ( + 'ca', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='django_x509.Ca', + verbose_name='CA', + ), + ), ], options={ 'verbose_name': 'certificate', @@ -78,7 +326,6 @@ class Migration(migrations.Migration): }, ), migrations.AlterUniqueTogether( - name='cert', - unique_together=set([('ca', 'serial_number')]), + name='cert', unique_together=set([('ca', 'serial_number')]) ), ] diff --git a/django_x509/migrations/0002_certificate.py b/django_x509/migrations/0002_certificate.py index 0c7322e..a39b1ba 100644 --- a/django_x509/migrations/0002_certificate.py +++ b/django_x509/migrations/0002_certificate.py @@ -7,59 +7,115 @@ class Migration(migrations.Migration): - dependencies = [ - ('django_x509', '0001_initial'), - ] + dependencies = [('django_x509', '0001_initial')] operations = [ migrations.RenameField( - model_name='ca', - old_name='public_key', - new_name='certificate', + model_name='ca', old_name='public_key', new_name='certificate' ), migrations.RenameField( - model_name='cert', - old_name='public_key', - new_name='certificate', + model_name='cert', old_name='public_key', new_name='certificate' ), migrations.AlterField( model_name='ca', name='digest', - field=models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm'), + field=models.CharField( + blank=True, + choices=[ + ('', ''), + ('sha1', 'SHA1'), + ('sha224', 'SHA224'), + ('sha256', 'SHA256'), + ('sha384', 'SHA384'), + ('sha512', 'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), ), migrations.AlterField( model_name='ca', name='key_length', - field=models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length'), + field=models.CharField( + blank=True, + choices=[ + ('', ''), + ('512', '512'), + ('1024', '1024'), + ('2048', '2048'), + ('4096', '4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), ), migrations.AlterField( model_name='ca', name='private_key', - field=models.TextField(blank=True, help_text='private key in X.509 PEM format'), + field=models.TextField( + blank=True, help_text='private key in X.509 PEM format' + ), ), migrations.AlterField( model_name='cert', name='digest', - field=models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm'), + field=models.CharField( + blank=True, + choices=[ + ('', ''), + ('sha1', 'SHA1'), + ('sha224', 'SHA224'), + ('sha256', 'SHA256'), + ('sha384', 'SHA384'), + ('sha512', 'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), ), migrations.AlterField( model_name='cert', name='key_length', - field=models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length'), + field=models.CharField( + blank=True, + choices=[ + ('', ''), + ('512', '512'), + ('1024', '1024'), + ('2048', '2048'), + ('4096', '4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), ), migrations.AlterField( model_name='cert', name='private_key', - field=models.TextField(blank=True, help_text='private key in X.509 PEM format'), + field=models.TextField( + blank=True, help_text='private key in X.509 PEM format' + ), ), migrations.AlterField( model_name='ca', name='certificate', - field=models.TextField(blank=True, help_text='certificate in X.509 PEM format'), + field=models.TextField( + blank=True, help_text='certificate in X.509 PEM format' + ), ), migrations.AlterField( model_name='cert', name='certificate', - field=models.TextField(blank=True, help_text='certificate in X.509 PEM format'), + field=models.TextField( + blank=True, help_text='certificate in X.509 PEM format' + ), ), ] diff --git a/django_x509/migrations/0003_rename_organization_field.py b/django_x509/migrations/0003_rename_organization_field.py index b295fca..8c780ee 100644 --- a/django_x509/migrations/0003_rename_organization_field.py +++ b/django_x509/migrations/0003_rename_organization_field.py @@ -5,19 +5,13 @@ class Migration(migrations.Migration): - dependencies = [ - ('django_x509', '0002_certificate'), - ] + dependencies = [('django_x509', '0002_certificate')] operations = [ migrations.RenameField( - model_name='ca', - old_name='organization', - new_name='organization_name', + model_name='ca', old_name='organization', new_name='organization_name' ), migrations.RenameField( - model_name='cert', - old_name='organization', - new_name='organization_name', + model_name='cert', old_name='organization', new_name='organization_name' ), ] diff --git a/django_x509/migrations/0004_auto_20171207_1450.py b/django_x509/migrations/0004_auto_20171207_1450.py index 9b96e1d..f3bf6df 100644 --- a/django_x509/migrations/0004_auto_20171207_1450.py +++ b/django_x509/migrations/0004_auto_20171207_1450.py @@ -5,19 +5,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('django_x509', '0003_rename_organization_field'), - ] + dependencies = [('django_x509', '0003_rename_organization_field')] operations = [ migrations.AlterField( model_name='ca', name='serial_number', - field=models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=39, null=True, verbose_name='serial number'), + field=models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=39, + null=True, + verbose_name='serial number', + ), ), migrations.AlterField( model_name='cert', name='serial_number', - field=models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=39, null=True, verbose_name='serial number'), + field=models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=39, + null=True, + verbose_name='serial number', + ), ), ] diff --git a/django_x509/migrations/0005_organizational_unit_name.py b/django_x509/migrations/0005_organizational_unit_name.py index c337eb6..7608853 100644 --- a/django_x509/migrations/0005_organizational_unit_name.py +++ b/django_x509/migrations/0005_organizational_unit_name.py @@ -5,20 +5,21 @@ class Migration(migrations.Migration): - dependencies = [ - ('django_x509', '0004_auto_20171207_1450'), - ] + dependencies = [('django_x509', '0004_auto_20171207_1450')] operations = [ migrations.AddField( model_name='ca', name='organizational_unit_name', - field=models.CharField(blank=True, max_length=64, verbose_name='organizational unit name'), + field=models.CharField( + blank=True, max_length=64, verbose_name='organizational unit name' + ), ), migrations.AddField( model_name='cert', name='organizational_unit_name', - field=models.CharField(blank=True, max_length=64, verbose_name='organizational unit name'), + field=models.CharField( + blank=True, max_length=64, verbose_name='organizational unit name' + ), ), ] - diff --git a/django_x509/migrations/0006_passphrase_field.py b/django_x509/migrations/0006_passphrase_field.py index b849e61..3662cc2 100644 --- a/django_x509/migrations/0006_passphrase_field.py +++ b/django_x509/migrations/0006_passphrase_field.py @@ -5,19 +5,25 @@ class Migration(migrations.Migration): - dependencies = [ - ('django_x509', '0005_organizational_unit_name'), - ] + dependencies = [('django_x509', '0005_organizational_unit_name')] operations = [ migrations.AddField( model_name='ca', name='passphrase', - field=models.CharField(blank=True, help_text='Passphrase for the private key, if present', max_length=64), + field=models.CharField( + blank=True, + help_text='Passphrase for the private key, if present', + max_length=64, + ), ), migrations.AddField( model_name='cert', name='passphrase', - field=models.CharField(blank=True, help_text='Passphrase for the private key, if present', max_length=64), + field=models.CharField( + blank=True, + help_text='Passphrase for the private key, if present', + max_length=64, + ), ), ] diff --git a/django_x509/migrations/0007_serial_number_max_length.py b/django_x509/migrations/0007_serial_number_max_length.py index 6f45b45..bec53a4 100644 --- a/django_x509/migrations/0007_serial_number_max_length.py +++ b/django_x509/migrations/0007_serial_number_max_length.py @@ -5,19 +5,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('django_x509', '0006_passphrase_field'), - ] + dependencies = [('django_x509', '0006_passphrase_field')] operations = [ migrations.AlterField( model_name='ca', name='serial_number', - field=models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=48, null=True, verbose_name='serial number'), + field=models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=48, + null=True, + verbose_name='serial number', + ), ), migrations.AlterField( model_name='cert', name='serial_number', - field=models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=48, null=True, verbose_name='serial number'), + field=models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=48, + null=True, + verbose_name='serial number', + ), ), ] diff --git a/django_x509/models.py b/django_x509/models.py index 44195be..3e1f460 100644 --- a/django_x509/models.py +++ b/django_x509/models.py @@ -7,6 +7,7 @@ class Ca(AbstractCa): """ Concrete Ca model """ + class Meta(AbstractCa.Meta): abstract = False swappable = swapper.swappable_setting('django_x509', 'Ca') @@ -16,6 +17,7 @@ class Cert(AbstractCert): """ Concrete Cert model """ + class Meta(AbstractCert.Meta): abstract = False swappable = swapper.swappable_setting('django_x509', 'Cert') diff --git a/django_x509/settings.py b/django_x509/settings.py index eab7db6..2e308e7 100644 --- a/django_x509/settings.py +++ b/django_x509/settings.py @@ -3,11 +3,21 @@ DEFAULT_CERT_VALIDITY = getattr(settings, 'DJANGO_X509_DEFAULT_CERT_VALIDITY', 365) DEFAULT_CA_VALIDITY = getattr(settings, 'DJANGO_X509_DEFAULT_CA_VALIDITY', 3650) DEFAULT_KEY_LENGTH = str(getattr(settings, 'DJANGO_X509_DEFAULT_KEY_LENGTH', '2048')) -DEFAULT_DIGEST_ALGORITHM = getattr(settings, 'DJANGO_X509_DEFAULT_DIGEST_ALGORITHM', 'sha256') -CA_BASIC_CONSTRAINTS_CRITICAL = getattr(settings, 'DJANGO_X509_CA_BASIC_CONSTRAINTS_CRITICAL', True) -CA_BASIC_CONSTRAINTS_PATHLEN = getattr(settings, 'DJANGO_X509_CA_BASIC_CONSTRAINTS_PATHLEN', 0) +DEFAULT_DIGEST_ALGORITHM = getattr( + settings, 'DJANGO_X509_DEFAULT_DIGEST_ALGORITHM', 'sha256' +) +CA_BASIC_CONSTRAINTS_CRITICAL = getattr( + settings, 'DJANGO_X509_CA_BASIC_CONSTRAINTS_CRITICAL', True +) +CA_BASIC_CONSTRAINTS_PATHLEN = getattr( + settings, 'DJANGO_X509_CA_BASIC_CONSTRAINTS_PATHLEN', 0 +) CA_KEYUSAGE_CRITICAL = getattr(settings, 'DJANGO_X509_CA_KEYUSAGE_CRITICAL', True) -CA_KEYUSAGE_VALUE = getattr(settings, 'DJANGO_X509_CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign') +CA_KEYUSAGE_VALUE = getattr( + settings, 'DJANGO_X509_CA_KEYUSAGE_VALUE', 'cRLSign, keyCertSign' +) CERT_KEYUSAGE_CRITICAL = getattr(settings, 'DJANGO_X509_CERT_KEYUSAGE_CRITICAL', False) -CERT_KEYUSAGE_VALUE = getattr(settings, 'DJANGO_X509_CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment') # noqa +CERT_KEYUSAGE_VALUE = getattr( + settings, 'DJANGO_X509_CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment' +) # noqa CRL_PROTECTED = getattr(settings, 'DJANGO_X509_CRL_PROTECTED', False) diff --git a/django_x509/tests/test_admin.py b/django_x509/tests/test_admin.py index 2c8e099..50dc728 100644 --- a/django_x509/tests/test_admin.py +++ b/django_x509/tests/test_admin.py @@ -19,78 +19,86 @@ def has_perm(self, perm): request = MessagingRequest() request.user = MockSuperUser() -ca_fields = ['operation_type', - 'name', - 'notes', - 'key_length', - 'digest', - 'validity_start', - 'validity_end', - 'country_code', - 'state', - 'city', - 'organization_name', - 'organizational_unit_name', - 'email', - 'common_name', - 'extensions', - 'serial_number', - 'certificate', - 'private_key', - 'passphrase'] - -cert_fields = ['operation_type', - 'name', - 'ca', - 'notes', - 'key_length', - 'digest', - 'validity_start', - 'validity_end', - 'country_code', - 'state', - 'city', - 'organization_name', - 'organizational_unit_name', - 'email', - 'common_name', - 'extensions', - 'serial_number', - 'certificate', - 'private_key', - 'passphrase'] - -ca_readonly = ['key_length', - 'digest', - 'validity_start', - 'validity_end', - 'country_code', - 'state', - 'city', - 'organization_name', - 'organizational_unit_name', - 'email', - 'common_name', - 'serial_number', - 'certificate', - 'private_key', - 'created', - 'modified'] - -cert_readonly = ['revoked', - 'revoked_at', - 'created', - 'modified', - 'created', - 'modified', - 'created', - 'modified', - 'created', - 'modified', - 'created', - 'modified', - 'created', - 'modified'] +ca_fields = [ + 'operation_type', + 'name', + 'notes', + 'key_length', + 'digest', + 'validity_start', + 'validity_end', + 'country_code', + 'state', + 'city', + 'organization_name', + 'organizational_unit_name', + 'email', + 'common_name', + 'extensions', + 'serial_number', + 'certificate', + 'private_key', + 'passphrase', +] + +cert_fields = [ + 'operation_type', + 'name', + 'ca', + 'notes', + 'key_length', + 'digest', + 'validity_start', + 'validity_end', + 'country_code', + 'state', + 'city', + 'organization_name', + 'organizational_unit_name', + 'email', + 'common_name', + 'extensions', + 'serial_number', + 'certificate', + 'private_key', + 'passphrase', +] + +ca_readonly = [ + 'key_length', + 'digest', + 'validity_start', + 'validity_end', + 'country_code', + 'state', + 'city', + 'organization_name', + 'organizational_unit_name', + 'email', + 'common_name', + 'serial_number', + 'certificate', + 'private_key', + 'created', + 'modified', +] + +cert_readonly = [ + 'revoked', + 'revoked_at', + 'created', + 'modified', + 'created', + 'modified', + 'created', + 'modified', + 'created', + 'modified', + 'created', + 'modified', + 'created', + 'modified', +] class ModelAdminTests(TestCase): @@ -161,11 +169,16 @@ def test_readonly_fields_Cert(self): ma = self.cert_admin(Cert, self.site) self.assertEqual(ma.get_readonly_fields(request), cert_readonly) ca_readonly.append('ca') - self.assertEqual(ma.get_readonly_fields(request, self.cert), tuple(ca_readonly + cert_readonly)) + self.assertEqual( + ma.get_readonly_fields(request, self.cert), + tuple(ca_readonly + cert_readonly), + ) def test_ca_url(self): ma = self.cert_admin(Cert, self.site) - self.assertEqual(ma.ca_url(self.cert), f'') + self.assertEqual( + ma.ca_url(self.cert), f'' + ) def test_revoke_action(self): ma = self.cert_admin(Cert, self.site) @@ -176,8 +189,8 @@ def test_revoke_action(self): def test_renew_ca_action(self): req = deepcopy(request) - ca = Ca.objects.create(name="test_ca") - cert = Cert.objects.create(name="test_cert", ca=ca) + ca = Ca.objects.create(name='test_ca') + cert = Cert.objects.create(name='test_cert', ca=ca) old_ca_cert = ca.certificate old_ca_key = ca.private_key old_ca_end = ca.validity_end @@ -187,14 +200,10 @@ def test_renew_ca_action(self): old_cert_end = cert.validity_end old_cert_serial_number = cert.serial_number ma = self.ca_admin(Ca, self.site) - req.POST.update({ - 'post': None - }) + req.POST.update({'post': None}) r = ma.renew_ca(req, [ca]) self.assertEqual(r.status_code, 200) - req.POST.update({ - 'post': 'yes' - }) + req.POST.update({'post': 'yes'}) ma.renew_ca(req, [ca]) message = req.get_message_strings() cert.refresh_from_db() @@ -207,13 +216,15 @@ def test_renew_ca_action(self): self.assertNotEqual(old_cert_key, cert.private_key) self.assertNotEqual(old_cert_cert, cert.certificate) self.assertEqual(len(message), 1) - self.assertEqual(message[0], - '1 CA and its related certificates have been successfully renewed') + self.assertEqual( + message[0], + '1 CA and its related certificates have been successfully renewed', + ) def test_renew_cert_action(self): req = deepcopy(request) - ca = Ca.objects.create(name="test_ca") - cert = Cert.objects.create(name="test_cert", ca=ca) + ca = Ca.objects.create(name='test_ca') + cert = Cert.objects.create(name='test_cert', ca=ca) old_ca_cert = ca.certificate old_ca_key = ca.private_key old_ca_end = ca.validity_end @@ -223,14 +234,10 @@ def test_renew_cert_action(self): old_cert_end = cert.validity_end old_cert_serial_number = cert.serial_number ma = self.cert_admin(Cert, self.site) - req.POST.update({ - 'post': None - }) + req.POST.update({'post': None}) r = ma.renew_cert(req, [cert]) self.assertEqual(r.status_code, 200) - req.POST.update({ - 'post': 'yes' - }) + req.POST.update({'post': 'yes'}) ma.renew_cert(req, [cert]) message = req.get_message_strings() self.assertNotEqual(old_cert_serial_number, cert.serial_number) diff --git a/django_x509/tests/test_ca.py b/django_x509/tests/test_ca.py index 1ab2e83..aa85c48 100644 --- a/django_x509/tests/test_ca.py +++ b/django_x509/tests/test_ca.py @@ -140,9 +140,13 @@ def test_import_ca(self): # verify field attribtues self.assertEqual(ca.key_length, '512') self.assertEqual(ca.digest, 'sha1') - start = timezone.make_aware(datetime.strptime('20150101000000Z', generalized_time)) + start = timezone.make_aware( + datetime.strptime('20150101000000Z', generalized_time) + ) self.assertEqual(ca.validity_start, start) - end = timezone.make_aware(datetime.strptime('20200101000000Z', generalized_time)) + end = timezone.make_aware( + datetime.strptime('20200101000000Z', generalized_time) + ) self.assertEqual(ca.validity_end, end) self.assertEqual(ca.country_code, 'US') self.assertEqual(ca.state, 'CA') @@ -156,8 +160,9 @@ def test_import_ca(self): self.assertEqual(cert.get_version(), 3) ca.delete() # test auto name - ca = Ca(certificate=self.import_certificate, - private_key=self.import_private_key) + ca = Ca( + certificate=self.import_certificate, private_key=self.import_private_key + ) ca.full_clean() ca.save() self.assertEqual(ca.name, 'importtest') @@ -222,10 +227,9 @@ def test_subject_key_identifier(self): e = ca.x509.get_extension(2) self.assertEqual(e.get_short_name().decode(), 'subjectKeyIdentifier') self.assertEqual(e.get_critical(), False) - e2 = crypto.X509Extension(b'subjectKeyIdentifier', - False, - b'hash', - subject=ca.x509) + e2 = crypto.X509Extension( + b'subjectKeyIdentifier', False, b'hash', subject=ca.x509 + ) self.assertEqual(e.get_data(), e2.get_data()) def test_authority_key_identifier(self): @@ -233,18 +237,20 @@ def test_authority_key_identifier(self): e = ca.x509.get_extension(3) self.assertEqual(e.get_short_name().decode(), 'authorityKeyIdentifier') self.assertEqual(e.get_critical(), False) - e2 = crypto.X509Extension(b'authorityKeyIdentifier', - False, - b'keyid:always,issuer:always', - issuer=ca.x509) + e2 = crypto.X509Extension( + b'authorityKeyIdentifier', + False, + b'keyid:always,issuer:always', + issuer=ca.x509, + ) self.assertEqual(e.get_data(), e2.get_data()) def test_extensions(self): extensions = [ { - "name": "nsComment", - "critical": False, - "value": "CA - autogenerated Certificate" + 'name': 'nsComment', + 'critical': False, + 'value': 'CA - autogenerated Certificate', } ] ca = self._create_ca(extensions=extensions) @@ -264,7 +270,7 @@ def test_extensions_error1(self): self.fail('ValidationError not raised') def test_extensions_error2(self): - extensions = [{"wrong": "wrong"}] + extensions = [{'wrong': 'wrong'}] try: self._create_ca(extensions=extensions) except ValidationError as e: @@ -287,17 +293,13 @@ def test_get_revoked_certs(self): # expired certificates are not counted start = now - timedelta(days=6650) end = now - timedelta(days=6600) - c4 = self._create_cert(ca=ca, - validity_start=start, - validity_end=end) + c4 = self._create_cert(ca=ca, validity_start=start, validity_end=end) c4.revoke() self.assertEqual(ca.get_revoked_certs().count(), 2) # inactive not counted yet start = now + timedelta(days=2) end = now + timedelta(days=365) - c5 = self._create_cert(ca=ca, - validity_start=start, - validity_end=end) + c5 = self._create_cert(ca=ca, validity_start=start, validity_end=end) c5.revoke() self.assertEqual(ca.get_revoked_certs().count(), 2) @@ -459,15 +461,17 @@ def test_fill_subject_non_strings(self): -----END RSA PRIVATE KEY-----""" def test_ca_invalid_country(self): - ca = self._create_ca(name='ImportTest error', - certificate=self.problematic_certificate, - private_key=self.problematic_private_key) + ca = self._create_ca( + name='ImportTest error', + certificate=self.problematic_certificate, + private_key=self.problematic_private_key, + ) self.assertEqual(ca.country_code, '') def test_import_ca_cert_validation_error(self): certificate = self.import_certificate[20:] private_key = self.import_private_key - ca = Ca(name="TestCaCertValidation") + ca = Ca(name='TestCaCertValidation') try: ca.certificate = certificate ca.private_key = private_key @@ -476,9 +480,10 @@ def test_import_ca_cert_validation_error(self): # cryptography 2.4 and 2.6 have different error message formats error_msg = str(e.message_dict['certificate'][0]) self.assertTrue( - "('PEM routines', 'PEM_read_bio', 'no start line')" in error_msg # cryptography 2.4+ - or - "('PEM routines', 'get_name', 'no start line')" in error_msg # cryptography 2.6+ + "('PEM routines', 'PEM_read_bio', 'no start line')" + in error_msg # cryptography 2.4+ + or "('PEM routines', 'get_name', 'no start line')" + in error_msg # cryptography 2.6+ ) else: self.fail('ValidationError not raised') @@ -486,7 +491,7 @@ def test_import_ca_cert_validation_error(self): def test_import_ca_key_validation_error(self): certificate = self.import_certificate private_key = self.import_private_key[20:] - ca = Ca(name="TestCaKeyValidation") + ca = Ca(name='TestCaKeyValidation') try: ca.certificate = certificate ca.private_key = private_key @@ -496,9 +501,10 @@ def test_import_ca_key_validation_error(self): # cryptography 2.4 and 2.6 have different error message formats error_msg = str(e.message_dict['private_key'][0]) self.assertTrue( - "('PEM routines', 'PEM_read_bio', 'no start line')" in error_msg # cryptography 2.4+ - or - "('PEM routines', 'get_name', 'no start line')" in error_msg # cryptography 2.6+ + "('PEM routines', 'PEM_read_bio', 'no start line')" + in error_msg # cryptography 2.4+ + or "('PEM routines', 'get_name', 'no start line')" + in error_msg # cryptography 2.6+ ) else: self.fail('ValidationError not raised') @@ -513,7 +519,10 @@ def test_bad_serial_number_ca(self): try: self._create_ca(serial_number='notIntegers') except ValidationError as e: - self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0])) + self.assertEqual( + 'Serial number must be an integer', + str(e.message_dict['serial_number'][0]), + ) def test_import_ca_key_with_passphrase(self): ca = Ca(name='ImportTest') @@ -605,8 +614,7 @@ def test_import_ca_key_with_incorrect_passphrase(self): ca.full_clean() ca.save() except ValidationError as e: - self.assertIn("Incorrect Passphrase", - str(e.message_dict['passphrase'][0])) + self.assertIn('Incorrect Passphrase', str(e.message_dict['passphrase'][0])) else: self.fail('ValidationError not raised') @@ -619,10 +627,13 @@ def test_generate_ca_with_passphrase(self): def test_datetime_to_string(self): generalized_datetime = datetime(2050, 1, 1, 0, 0, 0, 0) utc_datetime = datetime(2049, 12, 31, 0, 0, 0, 0) - self.assertEqual(datetime_to_string(generalized_datetime), - generalized_datetime.strftime(generalized_time)) - self.assertEqual(datetime_to_string(utc_datetime), - utc_datetime.strftime(utc_time)) + self.assertEqual( + datetime_to_string(generalized_datetime), + generalized_datetime.strftime(generalized_time), + ) + self.assertEqual( + datetime_to_string(utc_datetime), utc_datetime.strftime(utc_time) + ) def test_renew(self): ca = self._create_ca() diff --git a/django_x509/tests/test_cert.py b/django_x509/tests/test_cert.py index 0d6a2d8..508cef9 100644 --- a/django_x509/tests/test_cert.py +++ b/django_x509/tests/test_cert.py @@ -152,10 +152,12 @@ def test_import_cert(self): ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() - cert = Cert(name='ImportCertTest', - ca=ca, - certificate=self.import_certificate, - private_key=self.import_private_key) + cert = Cert( + name='ImportCertTest', + ca=ca, + certificate=self.import_certificate, + private_key=self.import_private_key, + ) cert.full_clean() cert.save() x509 = cert.x509 @@ -178,9 +180,13 @@ def test_import_cert(self): # verify field attribtues self.assertEqual(cert.key_length, '512') self.assertEqual(cert.digest, 'sha1') - start = timezone.make_aware(datetime.strptime('20151101000000Z', generalized_time)) + start = timezone.make_aware( + datetime.strptime('20151101000000Z', generalized_time) + ) self.assertEqual(cert.validity_start, start) - end = timezone.make_aware(datetime.strptime('21181102180025Z', generalized_time)) + end = timezone.make_aware( + datetime.strptime('21181102180025Z', generalized_time) + ) self.assertEqual(cert.validity_end, end) self.assertEqual(cert.country_code, '') self.assertEqual(cert.state, '') @@ -193,9 +199,11 @@ def test_import_cert(self): self.assertEqual(x509.get_version(), 2) cert.delete() # test auto name - cert = Cert(certificate=self.import_certificate, - private_key=self.import_private_key, - ca=ca) + cert = Cert( + certificate=self.import_certificate, + private_key=self.import_private_key, + ca=ca, + ) cert.full_clean() cert.save() self.assertEqual(cert.name, '123456') @@ -206,8 +214,7 @@ def test_import_private_key_empty(self): ca.private_key = self.import_ca_private_key ca.full_clean() ca.save() - cert = Cert(name='ImportTest', - ca=ca) + cert = Cert(name='ImportTest', ca=ca) cert.certificate = self.import_certificate try: cert.full_clean() @@ -219,14 +226,16 @@ def test_import_private_key_empty(self): def test_import_wrong_ca(self): # test auto name - cert = Cert(certificate=self.import_certificate, - private_key=self.import_private_key, - ca=self._create_ca()) + cert = Cert( + certificate=self.import_certificate, + private_key=self.import_private_key, + ca=self._create_ca(), + ) try: cert.full_clean() except ValidationError as e: # verify error message - self.assertIn('CA doesn\'t match', str(e.message_dict['__all__'][0])) + self.assertIn("CA doesn't match", str(e.message_dict['__all__'][0])) else: self.fail('ValidationError not raised') @@ -251,17 +260,18 @@ def test_keyusage_value(self): e = cert.x509.get_extension(1) self.assertEqual(e.get_short_name().decode(), 'keyUsage') self.assertEqual(e.get_data(), b'\x03\x02\x07\x80') - setattr(app_settings, 'CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment') + setattr( + app_settings, 'CERT_KEYUSAGE_VALUE', 'digitalSignature, keyEncipherment' + ) def test_subject_key_identifier(self): cert = self._create_cert() e = cert.x509.get_extension(2) self.assertEqual(e.get_short_name().decode(), 'subjectKeyIdentifier') self.assertEqual(e.get_critical(), False) - e2 = crypto.X509Extension(b'subjectKeyIdentifier', - False, - b'hash', - subject=cert.x509) + e2 = crypto.X509Extension( + b'subjectKeyIdentifier', False, b'hash', subject=cert.x509 + ) self.assertEqual(e.get_data(), e2.get_data()) def test_authority_key_identifier(self): @@ -269,24 +279,22 @@ def test_authority_key_identifier(self): e = cert.x509.get_extension(3) self.assertEqual(e.get_short_name().decode(), 'authorityKeyIdentifier') self.assertEqual(e.get_critical(), False) - e2 = crypto.X509Extension(b'authorityKeyIdentifier', - False, - b'keyid:always,issuer:always', - issuer=cert.ca.x509) + e2 = crypto.X509Extension( + b'authorityKeyIdentifier', + False, + b'keyid:always,issuer:always', + issuer=cert.ca.x509, + ) self.assertEqual(e.get_data(), e2.get_data()) def test_extensions(self): extensions = [ + {'name': 'nsCertType', 'critical': False, 'value': 'client'}, { - "name": "nsCertType", - "critical": False, - "value": "client" + 'name': 'extendedKeyUsage', + 'critical': True, # critical just for testing purposes + 'value': 'clientAuth', }, - { - "name": "extendedKeyUsage", - "critical": True, # critical just for testing purposes - "value": "clientAuth" - } ] cert = self._create_cert(extensions=extensions) e1 = cert.x509.get_extension(4) @@ -309,9 +317,7 @@ def test_extensions_error1(self): self.fail('ValidationError not raised') def test_extensions_error2(self): - extensions = [ - {"wrong": "wrong"} - ] + extensions = [{'wrong': 'wrong'}] try: self._create_cert(extensions=extensions) except ValidationError as e: @@ -354,11 +360,7 @@ def test_cert_create(self): ca.full_clean() ca.save() - Cert.objects.create( - ca=ca, - common_name='TestCert1', - name='TestCert1', - ) + Cert.objects.create(ca=ca, common_name='TestCert1', name='TestCert1') def test_import_cert_validation_error(self): certificate = self.import_certificate[20:] @@ -369,18 +371,21 @@ def test_import_cert_validation_error(self): ca.full_clean() ca.save() try: - cert = Cert(name='TestCertValidation', - ca=ca, - certificate=certificate, - private_key=private_key) + cert = Cert( + name='TestCertValidation', + ca=ca, + certificate=certificate, + private_key=private_key, + ) cert.full_clean() except ValidationError as e: # cryptography 2.4 and 2.6 have different error message formats error_msg = str(e.message_dict['certificate'][0]) self.assertTrue( - "('PEM routines', 'PEM_read_bio', 'no start line')" in error_msg # cryptography 2.4+ - or - "('PEM routines', 'get_name', 'no start line')" in error_msg # cryptography 2.6+ + "('PEM routines', 'PEM_read_bio', 'no start line')" + in error_msg # cryptography 2.4+ + or "('PEM routines', 'get_name', 'no start line')" + in error_msg # cryptography 2.6+ ) else: self.fail('ValidationError not raised') @@ -394,18 +399,21 @@ def test_import_key_validation_error(self): ca.full_clean() ca.save() try: - cert = Cert(name='TestKeyValidation', - ca=ca, - certificate=certificate, - private_key=private_key) + cert = Cert( + name='TestKeyValidation', + ca=ca, + certificate=certificate, + private_key=private_key, + ) cert.full_clean() except ValidationError as e: # cryptography 2.4 and 2.6 have different error message formats error_msg = str(e.message_dict['private_key'][0]) self.assertTrue( - "('PEM routines', 'PEM_read_bio', 'no start line')" in error_msg # cryptography 2.4+ - or - "('PEM routines', 'get_name', 'no start line')" in error_msg # cryptography 2.6+ + "('PEM routines', 'PEM_read_bio', 'no start line')" + in error_msg # cryptography 2.4+ + or "('PEM routines', 'get_name', 'no start line')" + in error_msg # cryptography 2.6+ ) else: self.fail('ValidationError not raised') @@ -420,7 +428,10 @@ def test_bad_serial_number_cert(self): try: self._create_cert(serial_number='notIntegers') except ValidationError as e: - self.assertEqual("Serial number must be an integer", str(e.message_dict['serial_number'][0])) + self.assertEqual( + 'Serial number must be an integer', + str(e.message_dict['serial_number'][0]), + ) def test_serial_number_clash(self): ca = Ca(name='TestSerialClash') @@ -430,15 +441,19 @@ def test_serial_number_clash(self): cert = self._create_cert(serial_number=123456, ca=ca) cert.full_clean() cert.save() - _cert = Cert(name='TestClash', - ca=ca, - certificate=self.import_certificate, - private_key=self.import_private_key) + _cert = Cert( + name='TestClash', + ca=ca, + certificate=self.import_certificate, + private_key=self.import_private_key, + ) try: _cert.full_clean() except ValidationError as e: - self.assertEqual("Certificate with this CA and Serial number already exists.", - str(e.message_dict['__all__'][0])) + self.assertEqual( + 'Certificate with this CA and Serial number already exists.', + str(e.message_dict['__all__'][0]), + ) def test_import_cert_with_passphrase(self): ca = Ca(name='ImportTest') diff --git a/django_x509/urls.py b/django_x509/urls.py index c29df99..5c6abb9 100644 --- a/django_x509/urls.py +++ b/django_x509/urls.py @@ -1,7 +1,7 @@ -''' +""" There are no urls in this file. It has no functional use. This file exists only to maintain backward compatibility. -''' +""" app_name = 'x509' # pragma: no cover -urlpatterns = [] # pragma: no cover +urlpatterns = [] # pragma: no cover diff --git a/run-qa-checks b/run-qa-checks index ab8db1b..b5ea7c6 100755 --- a/run-qa-checks +++ b/run-qa-checks @@ -2,18 +2,18 @@ set -e -openwisp-utils-qa-checks --migrations-to-ignore 4 \ - --migration-path ./django_x509/migrations/ \ - --migration-module django_x509 - +openwisp-qa-check --migrations-to-ignore 4 \ + --migration-path ./django_x509/migrations/ \ + --migration-module django_x509 echo '' echo 'Running checks for SAMPLE_APP' -SAMPLE_APP=1 openwisp-utils-qa-checks --skip-isort \ - --skip-flake8 \ - --skip-checkmigrations \ - --skip-checkendline \ - --skip-checkcommit \ - --migration-path ./tests/openwisp2/sample_x509/migrations/ \ - --migration-module sample_x509 +SAMPLE_APP=1 openwisp-qa-check --skip-isort \ + --skip-flake8 \ + --skip-black \ + --skip-checkmigrations \ + --skip-checkendline \ + --skip-checkcommit \ + --migration-path ./tests/openwisp2/sample_x509/migrations/ \ + --migration-module sample_x509 diff --git a/runtests.py b/runtests.py index 9d38c4e..83c7f16 100755 --- a/runtests.py +++ b/runtests.py @@ -9,6 +9,7 @@ if __name__ == '__main__': from django.core.management import execute_from_command_line + args = sys.argv args.insert(1, 'test') if not os.environ.get('SAMPLE_APP', False): diff --git a/setup.cfg b/setup.cfg index 81ad505..a95366f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,9 +5,19 @@ universal=1 skip = migrations known_first_party=django_x509 default_section = THIRDPARTY +recursive = 1 +line_length=88 +multi_line_output=3 +use_parentheses=True +include_trailing_comma=True +force_grid_wrap=0 [flake8] +# W503: line break before or after operator +# W504: line break after or after operator +# W605: invalid escape sequence +ignore = W605, W503, W504 exclude = *migrations*, ./tests/*settings*.py, setup.py -max-line-length = 110 +max-line-length = 88 diff --git a/setup.py b/setup.py index 4dfdb4c..759f61b 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,12 @@ def get_install_requires(): requirements = [] for line in open('requirements.txt').readlines(): # skip to next iteration if comment or empty line - if line.startswith('#') or line == '' or line.startswith('http') or line.startswith('git'): + if ( + line.startswith('#') + or line == '' + or line.startswith('http') + or line.startswith('git') + ): continue # add line to requirements requirements.append(line) @@ -24,13 +29,13 @@ def get_install_requires(): if sys.argv[-1] == 'publish': # delete any *.pyc, *.pyo and __pycache__ os.system('find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf') - os.system("python setup.py sdist bdist_wheel") - os.system("twine upload -s dist/*") - os.system("rm -rf dist build") + os.system('python setup.py sdist bdist_wheel') + os.system('twine upload -s dist/*') + os.system('rm -rf dist build') args = {'version': get_version()} - print("You probably want to also tag the version now:") + print('You probably want to also tag the version now:') print(" git tag -a %(version)s -m 'version %(version)s'" % args) - print(" git push --tags") + print(' git push --tags') sys.exit() @@ -60,5 +65,5 @@ def get_install_requires(): 'Framework :: Django', 'Topic :: Security :: Cryptography', 'Programming Language :: Python :: 3', - ] + ], ) diff --git a/tests/openwisp2/local_settings.example.py b/tests/openwisp2/local_settings.example.py index c733cab..32b4a06 100644 --- a/tests/openwisp2/local_settings.example.py +++ b/tests/openwisp2/local_settings.example.py @@ -1,7 +1,7 @@ # RENAME THIS FILE TO local_settings.py IF YOU NEED TO CUSTOMIZE SOME SETTINGS # BUT DO NOT COMMIT -#DATABASES = { +# DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': 'x509.db', @@ -10,4 +10,4 @@ # 'HOST': '', # 'PORT': '' # }, -#} +# } diff --git a/tests/openwisp2/sample_x509/migrations/0001_initial.py b/tests/openwisp2/sample_x509/migrations/0001_initial.py index ba055f8..c06ad77 100644 --- a/tests/openwisp2/sample_x509/migrations/0001_initial.py +++ b/tests/openwisp2/sample_x509/migrations/0001_initial.py @@ -13,34 +13,168 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( name='Ca', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), - ('key_length', models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), - ('digest', models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), - ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), - ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_ca_validity_end, null=True)), + ( + 'key_length', + models.CharField( + blank=True, + choices=[ + ('', ''), + ('512', '512'), + ('1024', '1024'), + ('2048', '2048'), + ('4096', '4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), + ), + ( + 'digest', + models.CharField( + blank=True, + choices=[ + ('', ''), + ('sha1', 'SHA1'), + ('sha224', 'SHA224'), + ('sha256', 'SHA256'), + ('sha384', 'SHA384'), + ('sha512', 'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), + ), + ( + 'validity_start', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_validity_start, + null=True, + ), + ), + ( + 'validity_end', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_ca_validity_end, + null=True, + ), + ), ('country_code', models.CharField(blank=True, max_length=2)), - ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), - ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), - ('organization_name', models.CharField(blank=True, max_length=64, verbose_name='organization')), - ('organizational_unit_name', models.CharField(blank=True, max_length=64, verbose_name='organizational unit name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), - ('extensions', jsonfield.fields.JSONField(blank=True, default=list, dump_kwargs={'indent': 4}, help_text='additional x509 certificate extensions', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='extensions')), - ('serial_number', models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=48, null=True, verbose_name='serial number')), - ('certificate', models.TextField(blank=True, help_text='certificate in X.509 PEM format')), - ('private_key', models.TextField(blank=True, help_text='private key in X.509 PEM format')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('passphrase', models.CharField(blank=True, help_text='Passphrase for the private key, if present', max_length=64)), + ( + 'state', + models.CharField( + blank=True, max_length=64, verbose_name='state or province' + ), + ), + ( + 'city', + models.CharField(blank=True, max_length=64, verbose_name='city'), + ), + ( + 'organization_name', + models.CharField( + blank=True, max_length=64, verbose_name='organization' + ), + ), + ( + 'organizational_unit_name', + models.CharField( + blank=True, + max_length=64, + verbose_name='organizational unit name', + ), + ), + ( + 'email', + models.EmailField( + blank=True, max_length=254, verbose_name='email address' + ), + ), + ( + 'common_name', + models.CharField( + blank=True, max_length=63, verbose_name='common name' + ), + ), + ( + 'extensions', + jsonfield.fields.JSONField( + blank=True, + default=list, + dump_kwargs={'indent': 4}, + help_text='additional x509 certificate extensions', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='extensions', + ), + ), + ( + 'serial_number', + models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=48, + null=True, + verbose_name='serial number', + ), + ), + ( + 'certificate', + models.TextField( + blank=True, help_text='certificate in X.509 PEM format' + ), + ), + ( + 'private_key', + models.TextField( + blank=True, help_text='private key in X.509 PEM format' + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'passphrase', + models.CharField( + blank=True, + help_text='Passphrase for the private key, if present', + max_length=64, + ), + ), ('details', models.CharField(blank=True, max_length=64, null=True)), ], options={ @@ -54,29 +188,173 @@ class Migration(migrations.Migration): fields=[ ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), - ('key_length', models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), - ('digest', models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), - ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), - ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_cert_validity_end, null=True)), + ( + 'key_length', + models.CharField( + blank=True, + choices=[ + ('', ''), + ('512', '512'), + ('1024', '1024'), + ('2048', '2048'), + ('4096', '4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), + ), + ( + 'digest', + models.CharField( + blank=True, + choices=[ + ('', ''), + ('sha1', 'SHA1'), + ('sha224', 'SHA224'), + ('sha256', 'SHA256'), + ('sha384', 'SHA384'), + ('sha512', 'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), + ), + ( + 'validity_start', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_validity_start, + null=True, + ), + ), + ( + 'validity_end', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_cert_validity_end, + null=True, + ), + ), ('country_code', models.CharField(blank=True, max_length=2)), - ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), - ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), - ('organization_name', models.CharField(blank=True, max_length=64, verbose_name='organization')), - ('organizational_unit_name', models.CharField(blank=True, max_length=64, verbose_name='organizational unit name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), - ('extensions', jsonfield.fields.JSONField(blank=True, default=list, dump_kwargs={'indent': 4}, help_text='additional x509 certificate extensions', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='extensions')), - ('serial_number', models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=48, null=True, verbose_name='serial number')), - ('certificate', models.TextField(blank=True, help_text='certificate in X.509 PEM format')), - ('private_key', models.TextField(blank=True, help_text='private key in X.509 PEM format')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('passphrase', models.CharField(blank=True, help_text='Passphrase for the private key, if present', max_length=64)), + ( + 'state', + models.CharField( + blank=True, max_length=64, verbose_name='state or province' + ), + ), + ( + 'city', + models.CharField(blank=True, max_length=64, verbose_name='city'), + ), + ( + 'organization_name', + models.CharField( + blank=True, max_length=64, verbose_name='organization' + ), + ), + ( + 'organizational_unit_name', + models.CharField( + blank=True, + max_length=64, + verbose_name='organizational unit name', + ), + ), + ( + 'email', + models.EmailField( + blank=True, max_length=254, verbose_name='email address' + ), + ), + ( + 'common_name', + models.CharField( + blank=True, max_length=63, verbose_name='common name' + ), + ), + ( + 'extensions', + jsonfield.fields.JSONField( + blank=True, + default=list, + dump_kwargs={'indent': 4}, + help_text='additional x509 certificate extensions', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='extensions', + ), + ), + ( + 'serial_number', + models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=48, + null=True, + verbose_name='serial number', + ), + ), + ( + 'certificate', + models.TextField( + blank=True, help_text='certificate in X.509 PEM format' + ), + ), + ( + 'private_key', + models.TextField( + blank=True, help_text='private key in X.509 PEM format' + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'passphrase', + models.CharField( + blank=True, + help_text='Passphrase for the private key, if present', + max_length=64, + ), + ), ('revoked', models.BooleanField(default=False, verbose_name='revoked')), - ('revoked_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='revoked at')), + ( + 'revoked_at', + models.DateTimeField( + blank=True, default=None, null=True, verbose_name='revoked at' + ), + ), ('details', models.CharField(blank=True, max_length=64, null=True)), - ('fingerprint', models.CharField(max_length=64, primary_key=True, serialize=False, unique=True)), - ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sample_x509.Ca', verbose_name='CA')), + ( + 'fingerprint', + models.CharField( + max_length=64, primary_key=True, serialize=False, unique=True + ), + ), + ( + 'ca', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='sample_x509.Ca', + verbose_name='CA', + ), + ), ], options={ 'verbose_name': 'certificate', @@ -88,31 +366,178 @@ class Migration(migrations.Migration): migrations.CreateModel( name='Cert', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ( + 'id', + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name='ID', + ), + ), ('name', models.CharField(max_length=64)), ('notes', models.TextField(blank=True)), - ('key_length', models.CharField(blank=True, choices=[('', ''), ('512', '512'), ('1024', '1024'), ('2048', '2048'), ('4096', '4096')], default=django_x509.base.models.default_key_length, help_text='bits', max_length=6, verbose_name='key length')), - ('digest', models.CharField(blank=True, choices=[('', ''), ('sha1', 'SHA1'), ('sha224', 'SHA224'), ('sha256', 'SHA256'), ('sha384', 'SHA384'), ('sha512', 'SHA512')], default=django_x509.base.models.default_digest_algorithm, help_text='bits', max_length=8, verbose_name='digest algorithm')), - ('validity_start', models.DateTimeField(blank=True, default=django_x509.base.models.default_validity_start, null=True)), - ('validity_end', models.DateTimeField(blank=True, default=django_x509.base.models.default_cert_validity_end, null=True)), + ( + 'key_length', + models.CharField( + blank=True, + choices=[ + ('', ''), + ('512', '512'), + ('1024', '1024'), + ('2048', '2048'), + ('4096', '4096'), + ], + default=django_x509.base.models.default_key_length, + help_text='bits', + max_length=6, + verbose_name='key length', + ), + ), + ( + 'digest', + models.CharField( + blank=True, + choices=[ + ('', ''), + ('sha1', 'SHA1'), + ('sha224', 'SHA224'), + ('sha256', 'SHA256'), + ('sha384', 'SHA384'), + ('sha512', 'SHA512'), + ], + default=django_x509.base.models.default_digest_algorithm, + help_text='bits', + max_length=8, + verbose_name='digest algorithm', + ), + ), + ( + 'validity_start', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_validity_start, + null=True, + ), + ), + ( + 'validity_end', + models.DateTimeField( + blank=True, + default=django_x509.base.models.default_cert_validity_end, + null=True, + ), + ), ('country_code', models.CharField(blank=True, max_length=2)), - ('state', models.CharField(blank=True, max_length=64, verbose_name='state or province')), - ('city', models.CharField(blank=True, max_length=64, verbose_name='city')), - ('organization_name', models.CharField(blank=True, max_length=64, verbose_name='organization')), - ('organizational_unit_name', models.CharField(blank=True, max_length=64, verbose_name='organizational unit name')), - ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), - ('common_name', models.CharField(blank=True, max_length=63, verbose_name='common name')), - ('extensions', jsonfield.fields.JSONField(blank=True, default=list, dump_kwargs={'indent': 4}, help_text='additional x509 certificate extensions', load_kwargs={'object_pairs_hook': collections.OrderedDict}, verbose_name='extensions')), - ('serial_number', models.CharField(blank=True, help_text='leave blank to determine automatically', max_length=48, null=True, verbose_name='serial number')), - ('certificate', models.TextField(blank=True, help_text='certificate in X.509 PEM format')), - ('private_key', models.TextField(blank=True, help_text='private key in X.509 PEM format')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('passphrase', models.CharField(blank=True, help_text='Passphrase for the private key, if present', max_length=64)), + ( + 'state', + models.CharField( + blank=True, max_length=64, verbose_name='state or province' + ), + ), + ( + 'city', + models.CharField(blank=True, max_length=64, verbose_name='city'), + ), + ( + 'organization_name', + models.CharField( + blank=True, max_length=64, verbose_name='organization' + ), + ), + ( + 'organizational_unit_name', + models.CharField( + blank=True, + max_length=64, + verbose_name='organizational unit name', + ), + ), + ( + 'email', + models.EmailField( + blank=True, max_length=254, verbose_name='email address' + ), + ), + ( + 'common_name', + models.CharField( + blank=True, max_length=63, verbose_name='common name' + ), + ), + ( + 'extensions', + jsonfield.fields.JSONField( + blank=True, + default=list, + dump_kwargs={'indent': 4}, + help_text='additional x509 certificate extensions', + load_kwargs={'object_pairs_hook': collections.OrderedDict}, + verbose_name='extensions', + ), + ), + ( + 'serial_number', + models.CharField( + blank=True, + help_text='leave blank to determine automatically', + max_length=48, + null=True, + verbose_name='serial number', + ), + ), + ( + 'certificate', + models.TextField( + blank=True, help_text='certificate in X.509 PEM format' + ), + ), + ( + 'private_key', + models.TextField( + blank=True, help_text='private key in X.509 PEM format' + ), + ), + ( + 'created', + model_utils.fields.AutoCreatedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='created', + ), + ), + ( + 'modified', + model_utils.fields.AutoLastModifiedField( + default=django.utils.timezone.now, + editable=False, + verbose_name='modified', + ), + ), + ( + 'passphrase', + models.CharField( + blank=True, + help_text='Passphrase for the private key, if present', + max_length=64, + ), + ), ('revoked', models.BooleanField(default=False, verbose_name='revoked')), - ('revoked_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='revoked at')), + ( + 'revoked_at', + models.DateTimeField( + blank=True, default=None, null=True, verbose_name='revoked at' + ), + ), ('details', models.CharField(blank=True, max_length=64, null=True)), - ('ca', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='sample_x509.Ca', verbose_name='CA')), + ( + 'ca', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='sample_x509.Ca', + verbose_name='CA', + ), + ), ], options={ 'verbose_name': 'certificate', diff --git a/tests/openwisp2/sample_x509/models.py b/tests/openwisp2/sample_x509/models.py index 2d61f50..c4f3237 100644 --- a/tests/openwisp2/sample_x509/models.py +++ b/tests/openwisp2/sample_x509/models.py @@ -14,6 +14,7 @@ class Ca(DetailsModel, AbstractCa): """ Concrete Ca model """ + class Meta(AbstractCa.Meta): abstract = False @@ -22,6 +23,7 @@ class Cert(DetailsModel, AbstractCert): """ Concrete Cert model """ + class Meta(AbstractCert.Meta): abstract = False @@ -30,6 +32,7 @@ class CustomCert(DetailsModel, AbstractCert): """ Custom Cert model """ + fingerprint = models.CharField(primary_key=True, max_length=64, unique=True) class Meta(AbstractCert.Meta): diff --git a/tests/openwisp2/settings.py b/tests/openwisp2/settings.py index e3c083e..f64fa1e 100644 --- a/tests/openwisp2/settings.py +++ b/tests/openwisp2/settings.py @@ -7,10 +7,7 @@ ALLOWED_HOSTS = [] DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'djangox509.db', - } + 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'djangox509.db'} } SECRET_KEY = 'fn)t*+$)ugeyip6-#txyy$5wf2ervc0d2n#h)qb)y5@ly$t*@w' @@ -61,7 +58,7 @@ 'django.contrib.messages.context_processors.messages', ], }, - }, + } ] DJANGO_X509_CA_MODEL = 'django_x509.Ca' diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index 02ca9a1..19fa3ed 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -2,8 +2,6 @@ from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns -urlpatterns = [ - url(r'^admin/', admin.site.urls), -] +urlpatterns = [url(r'^admin/', admin.site.urls)] urlpatterns += staticfiles_urlpatterns()