diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index 8b2669dbb8..e4be76164e 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -27,6 +27,7 @@ from rest_framework.fields import ( SerializerMethodField, ) from rest_framework.filters import OrderingFilter, SearchFilter +from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from rest_framework.validators import UniqueValidator @@ -42,7 +43,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg from authentik.crypto.models import CertificateKeyPair, KeyType from authentik.events.models import Event, EventAction from authentik.rbac.decorators import permission_required -from authentik.rbac.filters import ObjectFilter, SecretKeyFilter +from authentik.rbac.filters import SecretKeyFilter LOGGER = get_logger() @@ -292,6 +293,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): serializer = self.get_serializer(instance) return Response(serializer.data) + @permission_required("view_certificatekeypair_certificate") @extend_schema( parameters=[ OpenApiParameter( @@ -302,7 +304,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): ], responses={200: CertificateDataSerializer(many=False)}, ) - @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) + @action(detail=True, pagination_class=None, permission_classes=[IsAuthenticated]) def view_certificate(self, request: Request, pk: str) -> Response: """Return certificate-key pairs certificate and log access""" certificate: CertificateKeyPair = self.get_object() @@ -323,6 +325,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): return response return Response(CertificateDataSerializer({"data": certificate.certificate_data}).data) + @permission_required("view_certificatekeypair_key") @extend_schema( parameters=[ OpenApiParameter( @@ -333,7 +336,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet): ], responses={200: CertificateDataSerializer(many=False)}, ) - @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter]) + @action(detail=True, pagination_class=None, permission_classes=[IsAuthenticated]) def view_private_key(self, request: Request, pk: str) -> Response: """Return certificate-key pairs private key and log access""" certificate: CertificateKeyPair = self.get_object() diff --git a/authentik/crypto/migrations/0005_alter_certificatekeypair_options.py b/authentik/crypto/migrations/0005_alter_certificatekeypair_options.py new file mode 100644 index 0000000000..990b886587 --- /dev/null +++ b/authentik/crypto/migrations/0005_alter_certificatekeypair_options.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.8 on 2025-11-20 14:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_crypto", "0004_alter_certificatekeypair_name"), + ] + + operations = [ + migrations.AlterModelOptions( + name="certificatekeypair", + options={ + "permissions": [ + ( + "view_certificatekeypair_certificate", + "View Certificate-Key pair's certificate", + ), + ("view_certificatekeypair_key", "View Certificate-Key pair's private key"), + ], + "verbose_name": "Certificate-Key Pair", + "verbose_name_plural": "Certificate-Key Pairs", + }, + ), + ] diff --git a/authentik/crypto/models.py b/authentik/crypto/models.py index bdedfe9589..5c7b62f19a 100644 --- a/authentik/crypto/models.py +++ b/authentik/crypto/models.py @@ -140,3 +140,7 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel): class Meta: verbose_name = _("Certificate-Key Pair") verbose_name_plural = _("Certificate-Key Pairs") + permissions = [ + ("view_certificatekeypair_certificate", _("View Certificate-Key pair's certificate")), + ("view_certificatekeypair_key", _("View Certificate-Key pair's private key")), + ] diff --git a/authentik/crypto/tests.py b/authentik/crypto/tests.py index 81808f79ff..b064d866bb 100644 --- a/authentik/crypto/tests.py +++ b/authentik/crypto/tests.py @@ -9,10 +9,16 @@ from cryptography.x509.extensions import SubjectAlternativeName from cryptography.x509.general_name import DNSName from django.urls import reverse from django.utils.timezone import now +from guardian.shortcuts import assign_perm from rest_framework.test import APITestCase from authentik.core.api.used_by import DeleteAction -from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow +from authentik.core.tests.utils import ( + create_test_admin_user, + create_test_cert, + create_test_flow, + create_test_user, +) from authentik.crypto.api import CertificateKeyPairSerializer from authentik.crypto.builder import CertificateBuilder from authentik.crypto.models import CertificateKeyPair @@ -144,7 +150,7 @@ class TestCrypto(APITestCase): ), data={"name": cert.name}, ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) body = loads(response.content.decode()) api_cert = [x for x in body["results"] if x["name"] == cert.name][0] self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1) @@ -162,7 +168,7 @@ class TestCrypto(APITestCase): ), data={"name": cert.name, "has_key": False}, ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) body = loads(response.content.decode()) api_cert = [x for x in body["results"] if x["name"] == cert.name][0] self.assertEqual(api_cert["fingerprint_sha1"], cert.fingerprint_sha1) @@ -178,7 +184,7 @@ class TestCrypto(APITestCase): ), data={"name": cert.name, "include_details": False}, ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) body = loads(response.content.decode()) api_cert = [x for x in body["results"] if x["name"] == cert.name][0] self.assertEqual(api_cert["fingerprint_sha1"], None) @@ -186,15 +192,18 @@ class TestCrypto(APITestCase): def test_certificate_download(self): """Test certificate export (download)""" - self.client.force_login(create_test_admin_user()) keypair = create_test_cert() + user = create_test_user() + assign_perm("view_certificatekeypair", user, keypair) + assign_perm("view_certificatekeypair_certificate", user, keypair) + self.client.force_login(user) response = self.client.get( reverse( "authentik_api:certificatekeypair-view-certificate", kwargs={"pk": keypair.pk}, ) ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) response = self.client.get( reverse( "authentik_api:certificatekeypair-view-certificate", @@ -202,20 +211,23 @@ class TestCrypto(APITestCase): ), data={"download": True}, ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) self.assertIn("Content-Disposition", response) def test_private_key_download(self): """Test private_key export (download)""" - self.client.force_login(create_test_admin_user()) keypair = create_test_cert() + user = create_test_user() + assign_perm("view_certificatekeypair", user, keypair) + assign_perm("view_certificatekeypair_key", user, keypair) + self.client.force_login(user) response = self.client.get( reverse( "authentik_api:certificatekeypair-view-private-key", kwargs={"pk": keypair.pk}, ) ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) response = self.client.get( reverse( "authentik_api:certificatekeypair-view-private-key", @@ -223,12 +235,12 @@ class TestCrypto(APITestCase): ), data={"download": True}, ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) self.assertIn("Content-Disposition", response) def test_certificate_download_denied(self): """Test certificate export (download)""" - self.client.logout() + self.client.force_login(create_test_user()) keypair = create_test_cert() response = self.client.get( reverse( @@ -248,7 +260,7 @@ class TestCrypto(APITestCase): def test_private_key_download_denied(self): """Test private_key export (download)""" - self.client.logout() + self.client.force_login(create_test_user()) keypair = create_test_cert() response = self.client.get( reverse( @@ -284,7 +296,7 @@ class TestCrypto(APITestCase): kwargs={"pk": keypair.pk}, ) ) - self.assertEqual(200, response.status_code) + self.assertEqual(response.status_code, 200) self.assertJSONEqual( response.content.decode(), [ diff --git a/blueprints/schema.json b/blueprints/schema.json index e0c263bcc5..0dcd3ccbe3 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -5280,6 +5280,8 @@ "authentik_crypto.change_certificatekeypair", "authentik_crypto.delete_certificatekeypair", "authentik_crypto.view_certificatekeypair", + "authentik_crypto.view_certificatekeypair_certificate", + "authentik_crypto.view_certificatekeypair_key", "authentik_endpoints.add_connector", "authentik_endpoints.add_device", "authentik_endpoints.add_deviceaccessgroup", @@ -5953,7 +5955,9 @@ "add_certificatekeypair", "change_certificatekeypair", "delete_certificatekeypair", - "view_certificatekeypair" + "view_certificatekeypair", + "view_certificatekeypair_certificate", + "view_certificatekeypair_key" ] }, "user": { @@ -10604,6 +10608,8 @@ "authentik_crypto.change_certificatekeypair", "authentik_crypto.delete_certificatekeypair", "authentik_crypto.view_certificatekeypair", + "authentik_crypto.view_certificatekeypair_certificate", + "authentik_crypto.view_certificatekeypair_key", "authentik_endpoints.add_connector", "authentik_endpoints.add_device", "authentik_endpoints.add_deviceaccessgroup",