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:
parent
5af9a3d3be
commit
d7ad5f6a16
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
52
schema.yml
52
schema.yml
|
@ -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
|
||||||
|
|
Reference in New Issue