core: Improve service account creation (#4751)

* Added ability to select service account token expiration on creation

* Added call to user.set_unusable_password on service account creation

* Added forgotten call to save()

* Added and improved existsing tests

* Added accidentally deleted help text

* Fix lint
This commit is contained in:
sdimovv 2023-02-22 14:19:01 +02:00 committed by GitHub
parent 47e663f48c
commit c4e24c04f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 133 additions and 6 deletions

View File

@ -38,6 +38,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
BooleanField,
DateTimeField,
ListSerializer,
ModelSerializer,
PrimaryKeyRelatedField,
@ -353,6 +354,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
{
"name": CharField(required=True),
"create_group": BooleanField(default=False),
"expiring": BooleanField(default=True),
"expires": DateTimeField(
required=False,
help_text="If not provided, valid for 360 days",
),
},
),
responses={
@ -373,14 +379,20 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"""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)
expiring = request.data.get("expiring", True)
expires = request.data.get("expires", now() + timedelta(days=360))
with atomic():
try:
user = User.objects.create(
user: User = User.objects.create(
username=username,
name=username,
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
path=USER_PATH_SERVICE_ACCOUNT,
)
user.set_unusable_password()
user.save()
response = {
"username": user.username,
"user_uid": user.uid,
@ -396,7 +408,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
identifier=slugify(f"service-account-{username}-password"),
intent=TokenIntents.INTENT_APP_PASSWORD,
user=user,
expires=now() + timedelta(days=360),
expires=expires,
expiring=expiring,
)
response["token"] = token.key
return Response(response)

View File

@ -1,11 +1,19 @@
"""Test Users API"""
from datetime import datetime
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import AuthenticatedSession, User
from authentik.core.models import (
USER_ATTRIBUTE_SA,
USER_ATTRIBUTE_TOKEN_EXPIRING,
AuthenticatedSession,
Token,
User,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key
@ -130,7 +138,71 @@ class TestUsersAPI(APITestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(User.objects.filter(username="test-sa").exists())
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
self.assertTrue(token_filter.first().expiring)
def test_service_account_no_expire(self):
"""Service account creation without token expiration"""
self.client.force_login(self.admin)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": True,
"expiring": False,
},
)
self.assertEqual(response.status_code, 200)
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
self.assertFalse(token_filter.first().expiring)
def test_service_account_with_custom_expire(self):
"""Service account creation with custom token expiration date"""
self.client.force_login(self.admin)
expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone()
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={
"name": "test-sa",
"create_group": True,
"expires": expire_on.isoformat(),
},
)
self.assertEqual(response.status_code, 200)
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
token = token_filter.first()
self.assertTrue(token.expiring)
self.assertEqual(token.expires, expire_on)
def test_service_account_invalid(self):
"""Service account creation (twice with same name, expect error)"""
@ -143,7 +215,19 @@ class TestUsersAPI(APITestCase):
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(User.objects.filter(username="test-sa").exists())
user_filter = User.objects.filter(
username="test-sa",
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
)
self.assertTrue(user_filter.exists())
user: User = user_filter.first()
self.assertFalse(user.has_usable_password())
token_filter = Token.objects.filter(user=user)
self.assertTrue(token_filter.exists())
self.assertTrue(token_filter.first().expiring)
response = self.client.post(
reverse("authentik_api:user-service-account"),
data={

View File

@ -38310,6 +38310,13 @@ components:
create_group:
type: boolean
default: false
expiring:
type: boolean
default: true
expires:
type: string
format: date-time
description: If not provided, valid for 360 days
required:
- name
UserServiceAccountResponse:

View File

@ -1,4 +1,5 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { dateTimeLocal } from "@goauthentik/common/utils";
import { Form } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModalForm } from "@goauthentik/elements/forms/ModalForm";
@ -56,6 +57,28 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
${t`Enabling this toggle will create a group named after the user, with the user as member.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="expiring">
<label class="pf-c-switch">
<input class="pf-c-switch__input" type="checkbox" ?checked=${true} />
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${t`Expiring`}</span>
</label>
<p class="pf-c-form__helper-text">
${t`If this is selected, the token will expire. Upon expiration, the token will be rotated.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Expires on`} name="expires">
<input
type="datetime-local"
data-type="datetime-local"
value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
</form>`;
}