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:
parent
47e663f48c
commit
c4e24c04f6
|
@ -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)
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
|
|
Reference in New Issue