diff --git a/README.rst b/README.rst index 6a8e39ab4..691edd59e 100755 --- a/README.rst +++ b/README.rst @@ -952,7 +952,8 @@ Download template configuration GET /api/v1/controller/template/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific template. +The above endpoint triggers the download of a ``tar.gz`` file +containing the generated configuration for that specific template. Change details of template ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1003,7 +1004,8 @@ Download VPN configuration GET /api/v1/controller/vpn/{id}/configuration/ -The above endpoint triggers the download of a ``tar.gz`` file containing the generated configuration for that specific VPN. +The above endpoint triggers the download of a ``tar.gz`` file +containing the generated configuration for that specific VPN. Change details of VPN ^^^^^^^^^^^^^^^^^^^^^ @@ -1026,6 +1028,143 @@ Delete VPN DELETE /api/v1/controller/vpn/{id}/ +List CA +^^^^^^^ + +.. code-block:: text + + GET /api/v1/controller/ca/ + +Create new CA +^^^^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/ca/ + +Import existing CA +^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/ca/ + +**Note**: To import an existing CA, only ``name``, ``certificate`` +and ``private_key`` fields have to be filled in the ``HTML`` form or +included in the ``JSON`` format. + +Get CA Detail +^^^^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/controller/ca/{id}/ + +Change details of CA +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + PUT /api/v1/controller/ca/{id}/ + +Patch details of CA +^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + PATCH /api/v1/controller/ca/{id}/ + +Download CA(crl) +^^^^^^^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/controller/ca/{id}/crl/ + +The above endpoint triggers the download of ``{id}.crl`` file containing +up to date CRL of that specific CA. + +Delete CA +^^^^^^^^^ + +.. code-block:: text + + DELETE /api/v1/controller/ca/{id}/ + +Renew CA +^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/ca/{id}/renew/ + +List Cert +^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/controller/cert/ + +Create new Cert +^^^^^^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/cert/ + +Import existing Cert +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/cert/ + +**Note**: To import an existing Cert, only ``name``, ``ca``, +``certificate`` and ``private_key`` fields have to be filled +in the ``HTML`` form or included in the ``JSON`` format. + +Get Cert Detail +^^^^^^^^^^^^^^^ + +.. code-block:: text + + GET /api/v1/controller/cert/{id}/ + +Change details of Cert +^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + PUT /api/v1/controller/cert/{id}/ + +Patch details of Cert +^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + PATCH /api/v1/controller/cert/{id}/ + +Delete Cert +^^^^^^^^^^^ + +.. code-block:: text + + DELETE /api/v1/controller/cert/{id}/ + +Renew Cert +^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/cert/{id}/renew/ + +Revoke Cert +^^^^^^^^^^^ + +.. code-block:: text + + POST /api/v1/controller/cert/{id}/revoke/ + Default Alerts / Notifications ------------------------------ diff --git a/openwisp_controller/pki/api/serializers.py b/openwisp_controller/pki/api/serializers.py new file mode 100644 index 000000000..48b6c2288 --- /dev/null +++ b/openwisp_controller/pki/api/serializers.py @@ -0,0 +1,191 @@ +from django.utils.translation import ugettext_lazy as _ +from django_x509.base.models import ( + default_ca_validity_end, + default_cert_validity_end, + default_validity_start, +) +from rest_framework import serializers +from swapper import load_model + +from openwisp_users.api.mixins import FilterSerializerByOrgManaged +from openwisp_utils.api.serializers import ValidatedModelSerializer + +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') + + +class BaseSerializer(FilterSerializerByOrgManaged, ValidatedModelSerializer): + pass + + +class BaseListSerializer(BaseSerializer): + extensions = serializers.JSONField( + initial=[], + help_text=_('additional x509 certificate extensions'), + required=False, + ) + + def get_import_data_hook(self, instance): + return get_import_data(instance) + + def validate(self, data): + instance = self.instance or self.Meta.model(**data) + instance.full_clean() + if data.get('certificate') and data.get('private_key'): + data = self.get_import_data_hook(instance) + return data + + def validate_validity_start(self, value): + if value is None: + value = default_validity_start() + return value + + def default_validity_end_hook(self): + return default_ca_validity_end() + + def validate_validity_end(self, value): + if value is None: + value = self.default_validity_end_hook() + return value + + +class CaListSerializer(BaseListSerializer): + class Meta: + model = Ca + fields = [ + 'id', + 'name', + 'organization', + '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', + ] + read_only_fields = ['created', 'modified'] + extra_kwargs = { + 'organization': {'required': True}, + 'key_length': {'initial': '2048'}, + 'digest': {'initial': 'sha256'}, + 'passphrase': {'write_only': True}, + 'validity_start': {'default': default_validity_start()}, + 'validity_end': {'default': default_ca_validity_end()}, + } + + +def get_ca_detail_fields(fields): + """ + Returns the fields for the `CADetailSerializer`. + """ + fields.remove('passphrase') + return fields + + +class CaDetailSerializer(BaseSerializer): + extensions = serializers.JSONField(read_only=True) + + class Meta: + model = Ca + fields = get_ca_detail_fields(CaListSerializer.Meta.fields[:]) + read_only_fields = fields[4:] + + +class CaRenewSerializer(CaDetailSerializer): + class Meta(CaDetailSerializer.Meta): + read_only_fields = CaDetailSerializer.Meta.fields + + +def get_import_data(instance): + data = { + 'name': instance.name, + 'organization': instance.organization, + 'key_length': instance.key_length, + 'digest': instance.digest, + 'validity_start': instance.validity_start, + 'validity_end': instance.validity_end, + 'country_code': instance.country_code, + 'state': instance.state, + 'city': instance.city, + 'organization_name': instance.organization_name, + 'organizational_unit_name': instance.organizational_unit_name, + 'email': instance.email, + 'common_name': instance.common_name, + 'extensions': instance.extensions, + 'serial_number': instance.serial_number, + 'certificate': instance.certificate, + 'private_key': instance.private_key, + 'passphrase': instance.passphrase, + } + return data + + +def get_cert_list_fields(fields): + """ + Returns the fields for the `CertListSerializer`. + """ + fields.insert(3, 'ca') + fields.insert(5, 'revoked') + fields.insert(6, 'revoked_at') + return fields + + +class CertListSerializer(BaseListSerializer): + include_shared = True + + class Meta: + model = Cert + fields = get_cert_list_fields(CaListSerializer.Meta.fields[:]) + read_only_fields = ['created', 'modified'] + extra_kwargs = { + 'revoked': {'read_only': True}, + 'revoked_at': {'read_only': True}, + 'key_length': {'initial': '2048'}, + 'digest': {'initial': 'sha256'}, + 'passphrase': {'write_only': True}, + 'validity_start': {'default': default_validity_start()}, + 'validity_end': {'default': default_cert_validity_end()}, + } + + def get_import_data_hook(self, instance): + data = super().get_import_data_hook(instance) + data.update({'ca': instance.ca}) + return data + + def default_validity_end_hook(self): + return default_cert_validity_end() + + +def get_cert_detail_fields(fields): + """ + Returns the fields for the `CertDetailSerializer`. + """ + fields.remove('passphrase') + return fields + + +class CertDetailSerializer(BaseSerializer): + extensions = serializers.JSONField(read_only=True) + + class Meta: + model = Cert + fields = get_cert_detail_fields(CertListSerializer.Meta.fields[:]) + read_only_fields = ['ca'] + fields[5:] + + +class CertRevokeRenewSerializer(CertDetailSerializer): + class Meta(CertDetailSerializer.Meta): + read_only_fields = CertDetailSerializer.Meta.fields diff --git a/openwisp_controller/pki/api/urls.py b/openwisp_controller/pki/api/urls.py new file mode 100644 index 000000000..08016e836 --- /dev/null +++ b/openwisp_controller/pki/api/urls.py @@ -0,0 +1,42 @@ +from django.conf import settings +from django.urls import path + +from . import views as api_views + +app_name = 'openwisp_controller' + + +def get_pki_api_urls(api_views): + """ + returns:: all the API urls of the PKI app + """ + if getattr(settings, 'OPENWISP_CONTROLLER_PKI_API', True): + return [ + path('controller/ca/', api_views.ca_list, name='ca_list'), + path('controller/ca//', api_views.ca_detail, name='ca_detail'), + path('controller/ca//renew/', api_views.ca_renew, name='ca_renew'), + path( + 'controller/ca//crl', + api_views.crl_download, + name='crl_download', + ), + path('controller/cert/', api_views.cert_list, name='cert_list'), + path( + 'controller/cert//', api_views.cert_detail, name='cert_detail' + ), + path( + 'controller/cert//revoke/', + api_views.cert_revoke, + name='cert_revoke', + ), + path( + 'controller/cert//renew/', + api_views.cert_renew, + name='cert_renew', + ), + ] + else: + return [] + + +urlpatterns = get_pki_api_urls(api_views) diff --git a/openwisp_controller/pki/api/views.py b/openwisp_controller/pki/api/views.py new file mode 100644 index 000000000..4eda76b5a --- /dev/null +++ b/openwisp_controller/pki/api/views.py @@ -0,0 +1,126 @@ +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +from rest_framework import pagination, serializers +from rest_framework.authentication import SessionAuthentication +from rest_framework.generics import ( + GenericAPIView, + ListCreateAPIView, + RetrieveAPIView, + RetrieveUpdateDestroyAPIView, +) +from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated +from rest_framework.response import Response +from swapper import load_model + +from openwisp_users.api.authentication import BearerAuthentication +from openwisp_users.api.mixins import FilterByOrganizationManaged + +from .serializers import ( + CaDetailSerializer, + CaListSerializer, + CaRenewSerializer, + CertDetailSerializer, + CertListSerializer, + CertRevokeRenewSerializer, +) + +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') + + +class ListViewPagination(pagination.PageNumberPagination): + page_size = 10 + page_size_query_param = 'page_size' + max_page_size = 100 + + +class ProtectedAPIMixin(FilterByOrganizationManaged): + authentication_classes = [BearerAuthentication, SessionAuthentication] + permission_classes = [ + IsAuthenticated, + DjangoModelPermissions, + ] + + +class CaListCreateView(ProtectedAPIMixin, ListCreateAPIView): + serializer_class = CaListSerializer + queryset = Ca.objects.order_by('-created') + pagination_class = ListViewPagination + + +class CaDetailView(ProtectedAPIMixin, RetrieveUpdateDestroyAPIView): + serializer_class = CaDetailSerializer + queryset = Ca.objects.all() + + +class CaRenewView(ProtectedAPIMixin, GenericAPIView): + serializer_class = serializers.Serializer + queryset = Ca.objects.all() + + def post(self, request, pk): + """ + Renews the CA. + """ + instance = self.get_object() + instance.renew() + serializer = CaRenewSerializer(instance) + return Response(serializer.data, status=200) + + +class CrlDownloadView(ProtectedAPIMixin, RetrieveAPIView): + serializer_class = CaDetailSerializer + queryset = Ca.objects.none() + + def retrieve(self, request, *args, **kwargs): + instance = get_object_or_404(Ca, pk=kwargs['pk']) + return HttpResponse( + instance.crl, status=200, content_type='application/x-pem-file' + ) + + +class CertListCreateView(ProtectedAPIMixin, ListCreateAPIView): + serializer_class = CertListSerializer + queryset = Cert.objects.order_by('-created') + pagination_class = ListViewPagination + + +class CertDetailView(ProtectedAPIMixin, RetrieveUpdateDestroyAPIView): + serializer_class = CertDetailSerializer + queryset = Cert.objects.select_related('ca') + + +class CertRevokeRenewBaseView(ProtectedAPIMixin, GenericAPIView): + serializer_class = serializers.Serializer + queryset = Cert.objects.select_related('ca') + + +class CertRevokeView(CertRevokeRenewBaseView): + def post(self, request, pk): + """ + Revokes the Certificate. + """ + instance = self.get_object() + instance.revoke() + serializer = CertRevokeRenewSerializer(instance) + return Response(serializer.data, status=200) + + +class CertRenewView(CertRevokeRenewBaseView): + def post(self, request, pk): + """ + Renews the Certificate. + """ + instance = self.get_object() + instance.renew() + serializer = CertRevokeRenewSerializer(instance) + return Response(serializer.data, status=200) + + +ca_list = CaListCreateView.as_view() +ca_detail = CaDetailView.as_view() +ca_renew = CaRenewView.as_view() +cert_list = CertListCreateView.as_view() +cert_detail = CertDetailView.as_view() +crl_download = CrlDownloadView.as_view() +cert_revoke = CertRevokeView.as_view() +cert_renew = CertRenewView.as_view() diff --git a/openwisp_controller/pki/tests/test_api.py b/openwisp_controller/pki/tests/test_api.py new file mode 100644 index 000000000..d9c31724a --- /dev/null +++ b/openwisp_controller/pki/tests/test_api.py @@ -0,0 +1,293 @@ +from django.test import TestCase +from django.urls import reverse +from swapper import load_model + +from openwisp_controller.tests.utils import TestAdminMixin +from openwisp_users.tests.utils import TestOrganizationMixin +from openwisp_utils.tests import AssertNumQueriesSubTestMixin + +from .utils import TestPkiMixin + +Ca = load_model('django_x509', 'Ca') +Cert = load_model('django_x509', 'Cert') + + +class TestPkiApi( + AssertNumQueriesSubTestMixin, + TestAdminMixin, + TestPkiMixin, + TestOrganizationMixin, + TestCase, +): + def setUp(self): + super().setUp() + self._login() + + _get_ca_data = { + 'name': 'Test CA', + 'organization': None, + 'key_length': '2048', + 'digest': 'sha256', + } + + _get_cert_data = { + 'name': 'Test Cert', + 'organization': None, + 'ca': None, + 'key_length': '2048', + 'digest': 'sha256', + 'serial_number': "", + } + + def test_ca_post_api(self): + self.assertEqual(Ca.objects.count(), 0) + path = reverse('pki_api:ca_list') + data = self._get_ca_data.copy() + with self.assertNumQueries(4): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Ca.objects.count(), 1) + + def test_ca_post_with_extensions_field(self): + self.assertEqual(Ca.objects.count(), 0) + path = reverse('pki_api:ca_list') + data = self._get_ca_data.copy() + data['extensions'] = [] + with self.assertNumQueries(4): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(r.data['extensions'], []) + self.assertEqual(Ca.objects.count(), 1) + + def test_ca_import_post_api(self): + ca1 = self._create_ca() + path = reverse('pki_api:ca_list') + data = { + 'name': 'import-ca-test', + 'organization': self._get_org().pk, + 'certificate': ca1.certificate, + 'private_key': ca1.private_key, + } + with self.assertNumQueries(5): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Ca.objects.count(), 2) + self.assertEqual(r.data['serial_number'], str(ca1.serial_number)) + self.assertEqual(r.data['state'], ca1.state) + self.assertEqual(r.data['city'], ca1.city) + self.assertEqual(r.data['email'], ca1.email) + + def test_ca_post_with_date_none_api(self): + self.assertEqual(Ca.objects.count(), 0) + path = reverse('pki_api:ca_list') + data = { + 'name': 'test-ca', + 'organization': None, + 'validity_start': None, + 'validity_end': None, + } + with self.assertNumQueries(4): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Ca.objects.count(), 1) + + def test_ca_list_api(self): + self._create_ca() + path = reverse('pki_api:ca_list') + with self.assertNumQueries(4): + r = self.client.get(path) + self.assertEqual(r.status_code, 200) + self.assertNotIn('passphrase', r.content.decode('utf8')) + + def test_ca_detail_api(self): + ca1 = self._create_ca(name='ca1', organization=self._get_org()) + path = reverse('pki_api:ca_detail', args=[ca1.pk]) + with self.assertNumQueries(3): + r = self.client.get(path) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['id'], ca1.pk) + self.assertEqual(r.data['extensions'], []) + + def test_ca_put_api(self): + ca1 = self._create_ca(name='ca1', organization=self._get_org()) + path = reverse('pki_api:ca_detail', args=[ca1.pk]) + org2 = self._create_org() + data = {'name': 'change-ca1', 'organization': org2.pk, 'notes': 'change-notes'} + with self.assertNumQueries(6): + r = self.client.put(path, data, content_type='application/json') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['name'], 'change-ca1') + self.assertEqual(r.data['organization'], org2.pk) + self.assertEqual(r.data['notes'], 'change-notes') + + def test_ca_patch_api(self): + ca1 = self._create_ca(name='ca1', organization=self._get_org()) + path = reverse('pki_api:ca_detail', args=[ca1.pk]) + data = { + 'name': 'change-ca1', + } + with self.assertNumQueries(5): + r = self.client.patch(path, data, content_type='application/json') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['name'], 'change-ca1') + + def test_crl_download_api(self): + ca1 = self._create_ca(name='ca1', organization=self._get_org()) + path = reverse('pki_api:crl_download', args=[ca1.pk]) + with self.assertNumQueries(4): + r = self.client.get(path) + self.assertEqual(r.status_code, 200) + + def test_ca_delete_api(self): + ca1 = self._create_ca(name='ca1', organization=self._get_org()) + path = reverse('pki_api:ca_detail', args=[ca1.pk]) + with self.assertNumQueries(8): + r = self.client.delete(path) + self.assertEqual(r.status_code, 204) + self.assertEqual(Ca.objects.count(), 0) + + def test_ca_post_renew_api(self): + ca1 = self._create_ca(name='ca1', organization=self._get_org()) + old_serial_num = ca1.serial_number + path = reverse('pki_api:ca_renew', args=[ca1.pk]) + with self.assertNumQueries(5): + r = self.client.post(path) + ca1.refresh_from_db() + self.assertEqual(r.status_code, 200) + self.assertNotEqual(ca1.serial_number, old_serial_num) + self.assertNotEqual(r.data['serial_number'], old_serial_num) + + def test_cert_post_api(self): + path = reverse('pki_api:cert_list') + data = self._get_cert_data.copy() + data['ca'] = self._create_ca().pk + with self.assertNumQueries(9): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Cert.objects.count(), 1) + + def test_import_cert_post_api(self): + path = reverse('pki_api:cert_list') + ca1 = self._create_ca() + data = { + 'name': 'import-test-ca', + 'organization': self._get_org().pk, + 'ca': ca1.id, + 'serial_number': '', + 'certificate': ca1.certificate, + 'private_key': ca1.private_key, + } + with self.assertNumQueries(9): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Cert.objects.count(), 1) + self.assertEqual(r.data['serial_number'], str(ca1.serial_number)) + self.assertEqual(r.data['state'], ca1.state) + self.assertEqual(r.data['city'], ca1.city) + self.assertEqual(r.data['email'], ca1.email) + + def test_cert_post_with_extensions_field(self): + path = reverse('pki_api:cert_list') + data = self._get_cert_data.copy() + data['ca'] = self._create_ca().pk + data['extensions'] = [] + with self.assertNumQueries(9): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Cert.objects.count(), 1) + self.assertEqual(r.data['extensions'], []) + + def test_cert_post_with_date_none(self): + path = reverse('pki_api:cert_list') + data = { + 'name': 'test-cert', + 'ca': self._create_ca().pk, + 'serial_number': "", + 'validity_start': None, + 'validity_end': None, + } + with self.assertNumQueries(9): + r = self.client.post(path, data, content_type='application/json') + self.assertEqual(r.status_code, 201) + self.assertEqual(Cert.objects.count(), 1) + + def test_cert_list_api(self): + self._create_cert(name='cert1') + path = reverse('pki_api:cert_list') + with self.assertNumQueries(4): + r = self.client.get(path) + self.assertEqual(r.status_code, 200) + self.assertEqual(Cert.objects.count(), 1) + self.assertNotIn('passphrase', r.content.decode('utf8')) + + def test_cert_detail_api(self): + cert1 = self._create_cert(name='cert1') + path = reverse('pki_api:cert_detail', args=[cert1.pk]) + with self.assertNumQueries(3): + r = self.client.get(path) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['id'], cert1.pk) + self.assertEqual(r.data['extensions'], []) + + def test_cert_put_api(self): + cert1 = self._create_cert(name='cert1') + org2 = self._create_org() + path = reverse('pki_api:cert_detail', args=[cert1.pk]) + data = { + 'name': 'cert1-change', + 'organization': org2.pk, + 'notes': 'new-notes', + } + with self.assertNumQueries(8): + r = self.client.put(path, data, content_type='application/json') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['name'], 'cert1-change') + self.assertEqual(r.data['organization'], org2.pk) + self.assertEqual(r.data['notes'], 'new-notes') + + def test_cert_patch_api(self): + cert1 = self._create_cert(name='cert1') + path = reverse('pki_api:cert_detail', args=[cert1.pk]) + data = {'name': 'cert1-change'} + with self.assertNumQueries(7): + r = self.client.patch(path, data, content_type='application/json') + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['name'], 'cert1-change') + + def test_cert_delete_api(self): + cert1 = self._create_cert(name='cert1') + path = reverse('pki_api:cert_detail', args=[cert1.pk]) + with self.assertNumQueries(8): + r = self.client.delete(path) + self.assertEqual(r.status_code, 204) + self.assertEqual(Cert.objects.count(), 0) + + def test_ca_in_cert_detail_fields(self): + cert1 = self._create_cert(name='cert1') + path = reverse('pki_api:cert_detail', args=[cert1.pk]) + with self.assertNumQueries(3): + r = self.client.get(path) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.data['ca'], cert1.ca.id) + + def test_post_cert_renew_api(self): + cert1 = self._create_cert(name='cert1') + old_serial_num = cert1.serial_number + path = reverse('pki_api:cert_renew', args=[cert1.pk]) + with self.assertNumQueries(5): + r = self.client.post(path) + self.assertEqual(r.status_code, 200) + cert1.refresh_from_db() + self.assertNotEqual(cert1.serial_number, old_serial_num) + self.assertNotEqual(r.data['serial_number'], old_serial_num) + + def test_post_cert_revoke_api(self): + cert1 = self._create_cert(name='cert1') + self.assertFalse(cert1.revoked) + path = reverse('pki_api:cert_revoke', args=[cert1.pk]) + with self.assertNumQueries(5): + r = self.client.post(path) + cert1.refresh_from_db() + self.assertEqual(r.status_code, 200) + self.assertTrue(cert1.revoked) + self.assertTrue(r.data['revoked']) diff --git a/openwisp_controller/pki/tests/test_models.py b/openwisp_controller/pki/tests/test_models.py index c5de7d0c8..5582acb2b 100644 --- a/openwisp_controller/pki/tests/test_models.py +++ b/openwisp_controller/pki/tests/test_models.py @@ -4,6 +4,7 @@ from OpenSSL import crypto from swapper import load_model +from openwisp_controller.tests.utils import TestAdminMixin from openwisp_users.tests.utils import TestOrganizationMixin from .utils import TestPkiMixin @@ -12,7 +13,7 @@ Cert = load_model('django_x509', 'Cert') -class TestModels(TestPkiMixin, TestOrganizationMixin, TestCase): +class TestModels(TestAdminMixin, TestPkiMixin, TestOrganizationMixin, TestCase): def test_ca_creation_with_org(self): org = self._get_org() ca = self._create_ca(organization=org) @@ -46,6 +47,7 @@ def test_cert_validate_org_relation_no_rel(self): cert.full_clean() def test_crl_view(self): + self._login() ca = self._create_ca() response = self.client.get(reverse('admin:crl', args=[ca.pk])) self.assertEqual(response.status_code, 200) diff --git a/tests/openwisp2/urls.py b/tests/openwisp2/urls.py index 8895f674f..554473313 100644 --- a/tests/openwisp2/urls.py +++ b/tests/openwisp2/urls.py @@ -63,6 +63,7 @@ path('accounts/', include('openwisp_users.accounts.urls')), path('api/v1/', include('openwisp_utils.api.urls')), path('api/v1/', include(('openwisp_users.api.urls', 'users'), namespace='users')), + path('api/v1/', include(('openwisp_controller.pki.api.urls'), namespace='pki_api')), ] urlpatterns += staticfiles_urlpatterns()