diff --git a/authentik/api/v3/urls.py b/authentik/api/v3/urls.py index 0df2981c0..2b915643b 100644 --- a/authentik/api/v3/urls.py +++ b/authentik/api/v3/urls.py @@ -15,6 +15,7 @@ from authentik.api.v3.sentry import SentryTunnelView from authentik.api.views import APIBrowserView from authentik.core.api.applications import ApplicationViewSet from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet +from authentik.core.api.devices import DeviceViewSet from authentik.core.api.groups import GroupViewSet from authentik.core.api.propertymappings import PropertyMappingViewSet from authentik.core.api.providers import ProviderViewSet @@ -169,6 +170,7 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet) router.register("propertymappings/scope", ScopeMappingViewSet) router.register("propertymappings/notification", NotificationWebhookMappingViewSet) +router.register("authenticators/all", DeviceViewSet, basename="device") router.register("authenticators/duo", DuoDeviceViewSet) router.register("authenticators/sms", SMSDeviceViewSet) router.register("authenticators/static", StaticDeviceViewSet) diff --git a/authentik/core/api/devices.py b/authentik/core/api/devices.py new file mode 100644 index 000000000..758004a0a --- /dev/null +++ b/authentik/core/api/devices.py @@ -0,0 +1,36 @@ +"""Authenticator Devices API Views""" +from django_otp import devices_for_user +from django_otp.models import Device +from drf_spectacular.utils import extend_schema +from rest_framework.fields import CharField, IntegerField, SerializerMethodField +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import ViewSet + +from authentik.core.api.utils import MetaNameSerializer + + +class DeviceSerializer(MetaNameSerializer): + """Serializer for Duo authenticator devices""" + + pk = IntegerField() + name = CharField() + type = SerializerMethodField() + + def get_type(self, instance: Device) -> str: + """Get type of device""" + return instance._meta.label + + +class DeviceViewSet(ViewSet): + """Viewset for authenticator devices""" + + serializer_class = DeviceSerializer + permission_classes = [IsAuthenticated] + + @extend_schema(responses={200: DeviceSerializer(many=True)}) + def list(self, request: Request) -> Response: + """Get all devices for current user""" + devices = devices_for_user(request.user) + return Response(DeviceSerializer(devices, many=True).data) diff --git a/schema.yml b/schema.yml index 34b3251a9..eed2c0211 100644 --- a/schema.yml +++ b/schema.yml @@ -655,6 +655,27 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' + /authenticators/all/: + get: + operationId: authenticators_all_list + description: Get all devices for current user + tags: + - authenticators + security: + - authentik: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Device' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' /authenticators/duo/: get: operationId: authenticators_duo_list @@ -20169,6 +20190,29 @@ components: $ref: '#/components/schemas/FlowRequest' required: - name + Device: + type: object + description: Serializer for Duo authenticator devices + properties: + verbose_name: + type: string + readOnly: true + verbose_name_plural: + type: string + readOnly: true + pk: + type: integer + name: + type: string + type: + type: string + readOnly: true + required: + - name + - pk + - type + - verbose_name + - verbose_name_plural DeviceChallenge: type: object description: Single device challenge