diff --git a/authentik/api/schema.py b/authentik/api/schema.py index 976706c01..38284d7a6 100644 --- a/authentik/api/schema.py +++ b/authentik/api/schema.py @@ -31,7 +31,7 @@ VALIDATION_ERROR = build_object_type( "non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)), "code": build_standard_type(OpenApiTypes.STR), }, - required=["detail"], + required=[], additionalProperties={}, ) diff --git a/authentik/stages/authenticator_duo/api.py b/authentik/stages/authenticator_duo/api.py index ad2b091cc..74ad4919c 100644 --- a/authentik/stages/authenticator_duo/api.py +++ b/authentik/stages/authenticator_duo/api.py @@ -1,7 +1,8 @@ """AuthenticatorDuoStage API Views""" from django_filters.rest_framework.backends import DjangoFilterBackend from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiResponse, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema +from guardian.shortcuts import get_objects_for_user from rest_framework import mixins from rest_framework.decorators import action from rest_framework.filters import OrderingFilter, SearchFilter @@ -12,6 +13,7 @@ from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import GenericViewSet, ModelViewSet from authentik.api.authorization import OwnerFilter, OwnerPermissions +from authentik.api.decorators import permission_required from authentik.core.api.used_by import UsedByMixin from authentik.flows.api.stages import StageSerializer from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice @@ -71,6 +73,43 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet): return Response(status=204) return Response(status=420) + @permission_required( + "", ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"] + ) + @extend_schema( + parameters=[ + OpenApiParameter( + name="duo_user_id", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY + ), + OpenApiParameter( + name="username", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY + ), + ], + responses={ + 204: OpenApiResponse(description="Enrollment successful"), + 400: OpenApiResponse(description="Device exists already"), + }, + ) + @action(methods=["POST"], detail=True) + # pylint: disable=invalid-name,unused-argument + def import_devices(self, request: Request, pk: str) -> Response: + """Import duo devices into authentik""" + stage: AuthenticatorDuoStage = self.get_object() + users = get_objects_for_user(request.user, "authentik_core.view_user").filter( + username=request.query_params.get("username", "") + ) + if not users.exists(): + return Response(data={"non_field_errors": ["user does not exist"]}, status=400) + devices = DuoDevice.objects.filter( + duo_user_id=request.query_params.get("duo_user_id"), user=users.first(), stage=stage + ) + if devices.exists(): + return Response(data={"non_field_errors": ["device exists already"]}, status=400) + DuoDevice.objects.create( + duo_user_id=request.query_params.get("duo_user_id"), user=users.first(), stage=stage + ) + return Response(status=204) + class DuoDeviceSerializer(ModelSerializer): """Serializer for Duo authenticator devices""" diff --git a/schema.yml b/schema.yml index c4beb3afb..7c399edd1 100644 --- a/schema.yml +++ b/schema.yml @@ -13585,6 +13585,43 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' + /stages/authenticator/duo/{stage_uuid}/import_devices/: + post: + operationId: stages_authenticator_duo_import_devices_create + description: Import duo devices into authentik + parameters: + - in: query + name: duo_user_id + schema: + type: string + - in: path + name: stage_uuid + schema: + type: string + format: uuid + description: A UUID string identifying this Duo Authenticator Setup Stage. + required: true + - in: query + name: username + schema: + type: string + tags: + - stages + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AuthenticatorDuoStageRequest' + required: true + security: + - authentik: [] + responses: + '204': + description: Enrollment successful + '400': + description: Device exists already + '403': + $ref: '#/components/schemas/GenericError' /stages/authenticator/duo/{stage_uuid}/used_by/: get: operationId: stages_authenticator_duo_used_by_list @@ -29004,8 +29041,6 @@ components: code: type: string additionalProperties: {} - required: - - detail Version: type: object description: Get running and latest version. diff --git a/website/docs/flow/stages/authenticator_duo/index.md b/website/docs/flow/stages/authenticator_duo/index.md index 457df3ad1..5802f3f25 100644 --- a/website/docs/flow/stages/authenticator_duo/index.md +++ b/website/docs/flow/stages/authenticator_duo/index.md @@ -9,3 +9,20 @@ Go to Applications, click on Protect an Application and search for "Auth API". C Copy all of the integration key, secret key and API hostname, and paste them in the Stage form. Devices created reference the stage they were created with, since the API credentials are needed to authenticate. This also means when the stage is deleted, all devices are removed. + +## Importing users + +:::info +Due to the way the Duo API works, authentik cannot automatically import existing Duo users. +::: + +:::info +This API requires version 2021.10.1 or later +::: + +You can call the `/api/v3/stages/authenticator/duo/{stage_uuid}/import_devices/` endpoint ([see here](https://goauthentik.io/api/#post-/stages/authenticator/duo/-stage_uuid-/import_devices/)) using the following parameters: + +- `duo_user_id`: The Duo User's ID. This can be found in the Duo Admin Portal, navigating to the user list and clicking on a single user. Their ID is shown in th URL. +- `username`: The authentik user's username to assign the device to. + +Additionally, you need to pass `stage_uuid` which is the `authenticator_duo` stage, in which you entered your API credentials.