Skip to content

Commit

Permalink
[improvement] Add Swapper #86
Browse files Browse the repository at this point in the history
Closes #86
  • Loading branch information
atb00ker committed May 21, 2020
1 parent a5cd3df commit 05b5d22
Show file tree
Hide file tree
Showing 34 changed files with 551 additions and 306 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ install:
- python setup.py -q develop

before_script:
- openwisp-utils-qa-checks --migrations-to-ignore 4 --migration-path ./django_x509/migrations/ --migration-module django_x509
- ./run-qa-checks

script:
- jslint django_x509/static/django-x509/js/*.js
- coverage run --source=django_x509 runtests.py
- SAMPLE_APP=1 ./runtests.py

after_success:
coveralls
86 changes: 75 additions & 11 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ django-x509
.. image:: https://badge.fury.io/py/django-x509.svg
:target: http://badge.fury.io/py/django-x509

.. image:: docs/readme_x509.gif

------------

Simple reusable django app implementing x509 PKI certificates management.
Expand Down Expand Up @@ -327,6 +329,11 @@ be protected with authentication or not.
Extending django-x509
---------------------

The django app ``tests/openwisp2/sample_x509/`` adds some changes on
top of the ``django-x509`` module with the sole purpose of testing the
module's extensibility. It can be used as a sample for extending
``django-x509`` functionality in your own application.

*django-x509* provides a set of models and admin classes which can be imported,
extended and reused by third party apps.

Expand All @@ -337,6 +344,10 @@ base classes from django-x509 and add your customizations.
In order to help django find the static files and templates of *django-x509*,
you need to perform the steps described below.

**Premise**: if you plan on using a customized version of this module, we suggest
to start with it since the beginning, because migrating your data from the default
module to your extended version may be time consuming.

1. Install ``openwisp-utils``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -394,6 +405,20 @@ Add ``openwisp_utils.loaders.DependencyLoader`` to ``TEMPLATES`` in your ``setti
}
]
5. Add swapper configurations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Add the following to your ``settings.py``:

.. code-block:: python
# Setting models for swapper module
DJANGO_X509_CA_MODEL = '<YOUR_MODULE_NAME>.Ca'
DJANGO_X509_CERT_MODEL = '<YOUR_MODULE_NAME>.Cert'
Substitute ``<YOUR_MODULE_NAME>`` with your actual django app name
(also known as ``app_label``).

Extending models
~~~~~~~~~~~~~~~~

Expand All @@ -405,18 +430,15 @@ This example provides an example of how to extend the base models of
# models.py of your app
from django.db import models
from django_x509.base.models import AbstractCa, AbstractCert
# the model ``organizations.Organization`` is omitted for brevity
# if you are curious to see a real implementation, check out django-organizations
class OrganizationMixin(models.Model):
organization = models.ForeignKey('organizations.Organization')
class Meta:
abstract = True
class Ca(OrganizationMixin, AbstractCa):
class Meta(AbstractCa.Meta):
abstract = False
Expand All @@ -425,7 +447,6 @@ This example provides an example of how to extend the base models of
# your own validation logic here...
pass
class Cert(OrganizationMixin, AbstractCert):
ca = models.ForeignKey(Ca)
Expand All @@ -436,8 +457,8 @@ This example provides an example of how to extend the base models of
# your own validation logic here...
pass
Extending the admin
~~~~~~~~~~~~~~~~~~~
Extending admin
~~~~~~~~~~~~~~~

Following the previous `Organization` example, you can avoid duplicating the admin
code by importing the base admin classes and registering your models with.
Expand All @@ -446,26 +467,69 @@ code by importing the base admin classes and registering your models with.
# admin.py of your app
from django.contrib import admin
from django_x509.base.admin import CaAdmin as BaseCaAdmin
from django_x509.base.admin import CertAdmin as BaseCertAdmin
from .models import Ca, Cert
class CaAdmin(BaseCaAdmin):
# extend/modify the default behaviour here
pass
class CertAdmin(BaseCertAdmin):
# extend/modify the default behaviour here
pass
admin.site.register(Ca, CaAdmin)
admin.site.register(Cert, CertAdmin)
Extending AppConfig
~~~~~~~~~~~~~~~~~~~

Extending django-x509 app config is simple, you can
use the following:

.. code-block:: python
from django_x509.apps import DjangoX509Config
class SampleX509Config(DjangoX509Config):
name = 'MY_AWESOME_APP'
verbose_name = 'MY_AWESOME_APP'
Extending tests
~~~~~~~~~~~~~~~

If you want to extend the testcases from django-x509:

.. code-block:: python
import os
from django.test import TestCase
from swapper import load_model
from django_x509.tests.base.test_admin import AbstractModelAdminTests
from django_x509.tests.base.test_ca import AbstractTestCa
from django_x509.tests.base.test_cert import AbstractTestCert
from .admin import CaAdmin, CertAdmin
Ca = load_model('django_x509', 'Ca')
Cert = load_model('django_x509', 'Cert')
class ModelAdminTests(AbstractModelAdminTests, TestCase):
app_name = 'YOUR_AWESOME_APP_NAME'
ca_model = Ca
cert_model = Cert
ca_admin = CaAdmin
cert_admin = CertAdmin
class TestCert(AbstractTestCert, TestCase):
ca_model = Ca
cert_model = Cert
class TestCa(AbstractTestCa, TestCase):
ca_model = Ca
cert_model = Cert
Contributing
------------

Expand Down
5 changes: 4 additions & 1 deletion django_x509/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.contrib import admin
from swapper import load_model

from .base.admin import AbstractCaAdmin, AbstractCertAdmin
from .models import Ca, Cert

Ca = load_model('django_x509', 'Ca')
Cert = load_model('django_x509', 'Cert')


class CertAdmin(AbstractCertAdmin):
Expand Down
6 changes: 3 additions & 3 deletions django_x509/base/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ def get_context(self, data, ca_count=0, cert_count=0):
'title': _('Renew selected CAs'),
'ca_count': ca_count,
'cert_count': cert_count,
'cancel_url': 'django_x509_ca_changelist',
'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': 'django_x509_cert_changelist',
'cancel_url': f'{self.opts.app_label}_cert_changelist',
'action': 'renew_cert'
})
context.update({
Expand Down Expand Up @@ -211,7 +211,7 @@ class Media:

def ca_url(self, obj):
url = reverse('admin:{0}_ca_change'.format(self.opts.app_label), args=[obj.ca.pk])
return format_html("<a href='{url}'>{text}</a>",
return format_html('<a href="{url}">{text}</a>',
url=url,
text=obj.ca.name)
ca_url.short_description = 'CA'
Expand Down
17 changes: 10 additions & 7 deletions django_x509/base/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from datetime import datetime, timedelta

import OpenSSL
import swapper
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
Expand Down Expand Up @@ -153,7 +154,7 @@ class BaseX509(models.Model):
modified = AutoLastModifiedField(_('modified'), editable=True)
passphrase = models.CharField(max_length=64,
blank=True,
help_text=_("Passphrase for the private key, if present"))
help_text=_('Passphrase for the private key, if present'))

class Meta:
abstract = True
Expand Down Expand Up @@ -239,9 +240,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: <br>{0}'.format(str(e.args[0]).replace('), ', '), <br>').strip("[]"))
if "bad decrypt" in error:
error = "<b>Incorrect Passphrase</b> <br>" + error
error = 'OpenSSL error: <br>{0}'.format(str(e.args[0]).replace('), ', '), <br>').strip('[]'))
if 'bad decrypt' in error:
error = '<b>Incorrect Passphrase</b> <br>' + error
errors['passphrase'] = ValidationError(_(mark_safe(error)))
continue
errors[field] = ValidationError(_(mark_safe(error)))
Expand Down Expand Up @@ -284,13 +285,13 @@ 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):
"""
Expand Down Expand Up @@ -491,7 +492,9 @@ class AbstractCert(BaseX509):
"""
Abstract Cert model
"""
ca = models.ForeignKey('django_x509.Ca', on_delete=models.CASCADE, verbose_name=_('CA'))
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'),
Expand Down
4 changes: 4 additions & 0 deletions django_x509/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import swapper

from .base.models import AbstractCa, AbstractCert


Expand All @@ -7,6 +9,7 @@ class Ca(AbstractCa):
"""
class Meta(AbstractCa.Meta):
abstract = False
swappable = swapper.swappable_setting('django_x509', 'Ca')


class Cert(AbstractCert):
Expand All @@ -15,3 +18,4 @@ class Cert(AbstractCert):
"""
class Meta(AbstractCert.Meta):
abstract = False
swappable = swapper.swappable_setting('django_x509', 'Cert')
45 changes: 0 additions & 45 deletions django_x509/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,45 +0,0 @@
"""
test utilities shared among test classes
these mixins are reused also in openwisp2
change with care.
"""


class TestX509Mixin(object):
def _create_ca(self, **kwargs):
options = dict(name='Test CA',
key_length='2048',
digest='sha256',
country_code='IT',
state='RM',
city='Rome',
organization_name='OpenWISP',
email='test@test.com',
common_name='openwisp.org',
extensions=[])
options.update(kwargs)
ca = self.ca_model(**options)
ca.full_clean()
ca.save()
return ca

def _create_cert(self, **kwargs):
options = dict(name='TestCert',
ca=None,
key_length='2048',
digest='sha256',
country_code='IT',
state='RM',
city='Rome',
organization_name='Test',
email='test@test.com',
common_name='openwisp.org',
extensions=[])
options.update(kwargs)
# auto create CA if not supplied
if not options.get('ca'):
options['ca'] = self._create_ca()
cert = self.cert_model(**options)
cert.full_clean()
cert.save()
return cert
File renamed without changes.
56 changes: 56 additions & 0 deletions django_x509/tests/base/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from django.contrib.messages.storage.fallback import FallbackStorage
from django.http import HttpRequest


class MessagingRequest(HttpRequest):
session = 'session'

def __init__(self):
super().__init__()
self._messages = FallbackStorage(self)

def get_messages(self):
return getattr(self._messages, '_queued_messages')

def get_message_strings(self):
return [str(m) for m in self.get_messages()]


class TestX509Mixin(object):
def _create_ca(self, **kwargs):
options = dict(name='Test CA',
key_length='2048',
digest='sha256',
country_code='IT',
state='RM',
city='Rome',
organization_name='OpenWISP',
email='test@test.com',
common_name='openwisp.org',
extensions=[])
options.update(kwargs)
ca = self.ca_model(**options)
ca.full_clean()
ca.save()
return ca

def _create_cert(self, **kwargs):
options = dict(name='TestCert',
ca=None,
key_length='2048',
digest='sha256',
country_code='IT',
state='RM',
city='Rome',
organization_name='Test',
email='test@test.com',
common_name='openwisp.org',
extensions=[])
options.update(kwargs)
# auto create CA if not supplied
if not options.get('ca'):
options['ca'] = self._create_ca()
cert = self.cert_model(**options)
cert.full_clean()
cert.save()
return cert
Loading

0 comments on commit 05b5d22

Please sign in to comment.