From d7ad5f6a16070d98bb0dbc83e3d167781c73953e Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 24 Aug 2021 20:09:22 +0200 Subject: [PATCH] core: add API to create service account with token for app password Signed-off-by: Jens Langhammer --- authentik/core/api/users.py | 63 +++++++++++++++++++++++++- authentik/core/tests/test_users_api.py | 36 +++++++++++++++ schema.yml | 52 +++++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 5caa1bc9b..b13adab8f 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -3,13 +3,20 @@ from json import loads from typing import Optional from django.db.models.query import QuerySet +from django.db.transaction import atomic +from django.db.utils import IntegrityError from django.urls import reverse_lazy from django.utils.http import urlencode from django.utils.translation import gettext as _ from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter from django_filters.filterset import FilterSet from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field +from drf_spectacular.utils import ( + OpenApiParameter, + extend_schema, + extend_schema_field, + inline_serializer, +) from guardian.shortcuts import get_anonymous_user, get_objects_for_user from rest_framework.decorators import action from rest_framework.fields import CharField, JSONField, SerializerMethodField @@ -34,7 +41,14 @@ from authentik.core.api.groups import GroupSerializer from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER -from authentik.core.models import Group, Token, TokenIntents, User +from authentik.core.models import ( + USER_ATTRIBUTE_SA, + USER_ATTRIBUTE_TOKEN_EXPIRING, + Group, + Token, + TokenIntents, + User, +) from authentik.events.models import EventAction from authentik.stages.email.models import EmailStage from authentik.stages.email.tasks import send_mails @@ -220,6 +234,51 @@ class UserViewSet(UsedByMixin, ModelViewSet): ) return link, token + @permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"]) + @extend_schema( + request=inline_serializer( + "UserServiceAccountSerializer", + { + "name": CharField(required=True), + "create_group": BooleanField(default=False), + }, + ), + responses={ + 200: inline_serializer( + "UserServiceAccountResponse", + { + "username": CharField(required=True), + "token": CharField(required=True), + }, + ) + }, + ) + @action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[]) + def service_account(self, request: Request) -> Response: + """Create a new user account that is marked as a service account""" + username = request.data.get("name") + create_group = request.data.get("create_group", False) + with atomic(): + try: + user = User.objects.create( + username=username, + name=username, + attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False}, + ) + if create_group: + group = Group.objects.create( + name=username, + ) + group.users.add(user) + token = Token.objects.create( + identifier=f"service-account-{username}-password", + intent=TokenIntents.INTENT_APP_PASSWORD, + user=user, + ) + return Response({"username": user.username, "token": token.key}) + except (IntegrityError) as exc: + return Response(data={"non_field_errors": [str(exc)]}, status=400) + @extend_schema(responses={200: SessionUserSerializer(many=False)}) @action(detail=False, pagination_class=None, filter_backends=[]) # pylint: disable=invalid-name diff --git a/authentik/core/tests/test_users_api.py b/authentik/core/tests/test_users_api.py index 2567c9908..20dfbb227 100644 --- a/authentik/core/tests/test_users_api.py +++ b/authentik/core/tests/test_users_api.py @@ -105,3 +105,39 @@ class TestUsersAPI(APITestCase): + f"?email_stage={stage.pk}" ) self.assertEqual(response.status_code, 204) + + def test_service_account(self): + """Service account creation""" + self.client.force_login(self.admin) + response = self.client.post(reverse("authentik_api:user-service-account")) + self.assertEqual(response.status_code, 400) + response = self.client.post( + reverse("authentik_api:user-service-account"), + data={ + "name": "test-sa", + "create_group": True, + }, + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(User.objects.filter(username="test-sa").exists()) + + def test_service_account_invalid(self): + """Service account creation (twice with same name, expect error)""" + self.client.force_login(self.admin) + response = self.client.post( + reverse("authentik_api:user-service-account"), + data={ + "name": "test-sa", + "create_group": True, + }, + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(User.objects.filter(username="test-sa").exists()) + response = self.client.post( + reverse("authentik_api:user-service-account"), + data={ + "name": "test-sa", + "create_group": True, + }, + ) + self.assertEqual(response.status_code, 400) diff --git a/schema.yml b/schema.yml index 5ce5d85f7..27890948c 100644 --- a/schema.yml +++ b/schema.yml @@ -3282,6 +3282,38 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' + /api/v2beta/core/users/service_account/: + post: + operationId: core_users_service_account_create + description: Create a new user account that is marked as a service account + tags: + - core + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UserServiceAccountRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/UserServiceAccountRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/UserServiceAccountRequest' + required: true + security: + - authentik: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/UserServiceAccountResponse' + description: '' + '400': + $ref: '#/components/schemas/ValidationError' + '403': + $ref: '#/components/schemas/GenericError' /api/v2beta/core/users/update_self/: put: operationId: core_users_update_self_update @@ -30542,6 +30574,26 @@ components: required: - name - username + UserServiceAccountRequest: + type: object + properties: + name: + type: string + create_group: + type: boolean + default: false + required: + - name + UserServiceAccountResponse: + type: object + properties: + username: + type: string + token: + type: string + required: + - token + - username UserSetting: type: object description: Serializer for User settings for stages and sources