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 typing import Optional
from django.db.models.query import QuerySet 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.urls import reverse_lazy
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes 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 guardian.shortcuts import get_anonymous_user, get_objects_for_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField 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.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict 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.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.events.models import EventAction
from authentik.stages.email.models import EmailStage from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails from authentik.stages.email.tasks import send_mails
@ -220,6 +234,51 @@ class UserViewSet(UsedByMixin, ModelViewSet):
) )
return link, token 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)}) @extend_schema(responses={200: SessionUserSerializer(many=False)})
@action(detail=False, pagination_class=None, filter_backends=[]) @action(detail=False, pagination_class=None, filter_backends=[])
# pylint: disable=invalid-name # pylint: disable=invalid-name

View File

@ -105,3 +105,39 @@ class TestUsersAPI(APITestCase):
+ f"?email_stage={stage.pk}" + f"?email_stage={stage.pk}"
) )
self.assertEqual(response.status_code, 204) 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' $ref: '#/components/schemas/ValidationError'
'403': '403':
$ref: '#/components/schemas/GenericError' $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/: /api/v2beta/core/users/update_self/:
put: put:
operationId: core_users_update_self_update operationId: core_users_update_self_update
@ -30542,6 +30574,26 @@ components:
required: required:
- name - name
- username - 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: UserSetting:
type: object type: object
description: Serializer for User settings for stages and sources description: Serializer for User settings for stages and sources