core: add API to create service account with token for app password

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-08-24 20:09:22 +02:00
parent 5af9a3d3be
commit d7ad5f6a16
3 changed files with 149 additions and 2 deletions

View File

@ -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

View File

@ -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)

View File

@ -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