From d8c3b8bad2108e64afd03100abb4bb90b244cd1e Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Wed, 7 Aug 2024 14:09:49 +0200 Subject: [PATCH] stages/authenticator: add created, last_updated and last_used metadata (#10636) * stages/authenticator: add created, last_updated and last_used metadata Signed-off-by: Marc 'risson' Schmitt * lint Signed-off-by: Marc 'risson' Schmitt * also show for users Signed-off-by: Marc 'risson' Schmitt * set allow_null Signed-off-by: Jens Langhammer --------- Signed-off-by: Marc 'risson' Schmitt Signed-off-by: Jens Langhammer Co-authored-by: Jens Langhammer --- authentik/core/api/devices.py | 11 ++++++- authentik/stages/authenticator/__init__.py | 7 ++++- authentik/stages/authenticator/models.py | 7 +++-- ...created_duodevice_last_updated_and_more.py | 30 +++++++++++++++++++ ...created_smsdevice_last_updated_and_more.py | 30 +++++++++++++++++++ ...ated_staticdevice_last_updated_and_more.py | 30 +++++++++++++++++++ ...reated_totpdevice_last_updated_and_more.py | 30 +++++++++++++++++++ ...ed_webauthndevice_last_updated_and_more.py | 30 +++++++++++++++++++ schema.yml | 16 ++++++++++ web/src/admin/users/UserDevicesTable.ts | 14 ++++++++- .../user/user-settings/mfa/MFADevicesPage.ts | 9 ++++++ 11 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 authentik/stages/authenticator_duo/migrations/0006_duodevice_created_duodevice_last_updated_and_more.py create mode 100644 authentik/stages/authenticator_sms/migrations/0007_smsdevice_created_smsdevice_last_updated_and_more.py create mode 100644 authentik/stages/authenticator_static/migrations/0010_staticdevice_created_staticdevice_last_updated_and_more.py create mode 100644 authentik/stages/authenticator_totp/migrations/0011_totpdevice_created_totpdevice_last_updated_and_more.py create mode 100644 authentik/stages/authenticator_webauthn/migrations/0012_webauthndevice_created_webauthndevice_last_updated_and_more.py diff --git a/authentik/core/api/devices.py b/authentik/core/api/devices.py index 528d1775f7..555264fe15 100644 --- a/authentik/core/api/devices.py +++ b/authentik/core/api/devices.py @@ -2,7 +2,13 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema -from rest_framework.fields import BooleanField, CharField, IntegerField, SerializerMethodField +from rest_framework.fields import ( + BooleanField, + CharField, + DateTimeField, + IntegerField, + SerializerMethodField, +) from rest_framework.permissions import IsAdminUser, IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response @@ -20,6 +26,9 @@ class DeviceSerializer(MetaNameSerializer): name = CharField() type = SerializerMethodField() confirmed = BooleanField() + created = DateTimeField(read_only=True) + last_updated = DateTimeField(read_only=True) + last_used = DateTimeField(read_only=True, allow_null=True) def get_type(self, instance: Device) -> str: """Get type of device""" diff --git a/authentik/stages/authenticator/__init__.py b/authentik/stages/authenticator/__init__.py index 9601b13af3..da685aaf96 100644 --- a/authentik/stages/authenticator/__init__.py +++ b/authentik/stages/authenticator/__init__.py @@ -1,7 +1,12 @@ """Authenticator devices helpers""" +from typing import TYPE_CHECKING + from django.db import transaction +if TYPE_CHECKING: + from authentik.core.models import User + def verify_token(user, device_id, token): """ @@ -63,7 +68,7 @@ def match_token(user, token): return device -def devices_for_user(user, confirmed=True, for_verify=False): +def devices_for_user(user: "User", confirmed: bool | None = True, for_verify: bool = False): """ Return an iterable of all devices registered to the given user. diff --git a/authentik/stages/authenticator/models.py b/authentik/stages/authenticator/models.py index f7a6125c57..90da9a10cc 100644 --- a/authentik/stages/authenticator/models.py +++ b/authentik/stages/authenticator/models.py @@ -9,6 +9,7 @@ from django.utils import timezone from django.utils.functional import cached_property from authentik.core.models import User +from authentik.lib.models import CreatedUpdatedModel from authentik.stages.authenticator.util import random_number_token @@ -18,7 +19,7 @@ class DeviceManager(models.Manager): ``Device.objects``. """ - def devices_for_user(self, user, confirmed=None): + def devices_for_user(self, user: User, confirmed: bool | None = None): """ Returns a queryset for all devices of this class that belong to the given user. @@ -37,7 +38,7 @@ class DeviceManager(models.Manager): return devices -class Device(models.Model): +class Device(CreatedUpdatedModel): """ Abstract base model for a :term:`device` attached to a user. Plugins must subclass this to define their OTP models. @@ -85,6 +86,8 @@ class Device(models.Model): confirmed = models.BooleanField(default=True, help_text="Is this device ready for use?") + last_used = models.DateTimeField(null=True) + objects = DeviceManager() class Meta: diff --git a/authentik/stages/authenticator_duo/migrations/0006_duodevice_created_duodevice_last_updated_and_more.py b/authentik/stages/authenticator_duo/migrations/0006_duodevice_created_duodevice_last_updated_and_more.py new file mode 100644 index 0000000000..37ba2a730d --- /dev/null +++ b/authentik/stages/authenticator_duo/migrations/0006_duodevice_created_duodevice_last_updated_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.7 on 2024-07-25 16:28 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_duo", "0005_authenticatorduostage_friendly_name"), + ] + + operations = [ + migrations.AddField( + model_name="duodevice", + name="created", + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)), + preserve_default=False, + ), + migrations.AddField( + model_name="duodevice", + name="last_updated", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="duodevice", + name="last_used", + field=models.DateTimeField(null=True), + ), + ] diff --git a/authentik/stages/authenticator_sms/migrations/0007_smsdevice_created_smsdevice_last_updated_and_more.py b/authentik/stages/authenticator_sms/migrations/0007_smsdevice_created_smsdevice_last_updated_and_more.py new file mode 100644 index 0000000000..440258dac6 --- /dev/null +++ b/authentik/stages/authenticator_sms/migrations/0007_smsdevice_created_smsdevice_last_updated_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.7 on 2024-07-25 16:28 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_sms", "0006_authenticatorsmsstage_friendly_name"), + ] + + operations = [ + migrations.AddField( + model_name="smsdevice", + name="created", + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)), + preserve_default=False, + ), + migrations.AddField( + model_name="smsdevice", + name="last_updated", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="smsdevice", + name="last_used", + field=models.DateTimeField(null=True), + ), + ] diff --git a/authentik/stages/authenticator_static/migrations/0010_staticdevice_created_staticdevice_last_updated_and_more.py b/authentik/stages/authenticator_static/migrations/0010_staticdevice_created_staticdevice_last_updated_and_more.py new file mode 100644 index 0000000000..3ba394ec21 --- /dev/null +++ b/authentik/stages/authenticator_static/migrations/0010_staticdevice_created_staticdevice_last_updated_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.7 on 2024-07-25 16:28 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_static", "0009_throttling"), + ] + + operations = [ + migrations.AddField( + model_name="staticdevice", + name="created", + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)), + preserve_default=False, + ), + migrations.AddField( + model_name="staticdevice", + name="last_updated", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="staticdevice", + name="last_used", + field=models.DateTimeField(null=True), + ), + ] diff --git a/authentik/stages/authenticator_totp/migrations/0011_totpdevice_created_totpdevice_last_updated_and_more.py b/authentik/stages/authenticator_totp/migrations/0011_totpdevice_created_totpdevice_last_updated_and_more.py new file mode 100644 index 0000000000..c4cfb933b5 --- /dev/null +++ b/authentik/stages/authenticator_totp/migrations/0011_totpdevice_created_totpdevice_last_updated_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.7 on 2024-07-25 16:28 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_totp", "0010_alter_totpdevice_key"), + ] + + operations = [ + migrations.AddField( + model_name="totpdevice", + name="created", + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)), + preserve_default=False, + ), + migrations.AddField( + model_name="totpdevice", + name="last_updated", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="totpdevice", + name="last_used", + field=models.DateTimeField(null=True), + ), + ] diff --git a/authentik/stages/authenticator_webauthn/migrations/0012_webauthndevice_created_webauthndevice_last_updated_and_more.py b/authentik/stages/authenticator_webauthn/migrations/0012_webauthndevice_created_webauthndevice_last_updated_and_more.py new file mode 100644 index 0000000000..7e82a77cc0 --- /dev/null +++ b/authentik/stages/authenticator_webauthn/migrations/0012_webauthndevice_created_webauthndevice_last_updated_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.7 on 2024-07-25 16:28 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_stages_authenticator_webauthn", "0001_squashed_0011_webauthndevice_aaguid"), + ] + + operations = [ + migrations.AddField( + model_name="webauthndevice", + name="created", + field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(1, 1, 1, 0, 0)), + preserve_default=False, + ), + migrations.AddField( + model_name="webauthndevice", + name="last_updated", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="webauthndevice", + name="last_used", + field=models.DateTimeField(null=True), + ), + ] diff --git a/schema.yml b/schema.yml index 0ef79ec321..5c2ffc5701 100644 --- a/schema.yml +++ b/schema.yml @@ -36459,8 +36459,24 @@ components: readOnly: true confirmed: type: boolean + created: + type: string + format: date-time + readOnly: true + last_updated: + type: string + format: date-time + readOnly: true + last_used: + type: string + format: date-time + readOnly: true + nullable: true required: - confirmed + - created + - last_updated + - last_used - meta_model_name - name - pk diff --git a/web/src/admin/users/UserDevicesTable.ts b/web/src/admin/users/UserDevicesTable.ts index 0debd07ca1..6810ecb74b 100644 --- a/web/src/admin/users/UserDevicesTable.ts +++ b/web/src/admin/users/UserDevicesTable.ts @@ -1,5 +1,6 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { deviceTypeName } from "@goauthentik/common/labels"; +import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/elements/forms/DeleteBulkForm"; import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table"; @@ -44,7 +45,10 @@ export class UserDeviceTable extends Table { return [ msg("Name"), msg("Type"), - msg("Confirmed") + msg("Confirmed"), + msg("Created at"), + msg("Last updated at"), + msg("Last used at"), ].map((th) => new TableColumn(th, "")); } @@ -98,6 +102,14 @@ export class UserDeviceTable extends Table { html`${item.name}`, html`${deviceTypeName(item)}`, html`${item.confirmed ? msg("Yes") : msg("No")}`, + html`
${getRelativeTime(item.created)}
+ ${item.created.toLocaleString()}`, + html`
${getRelativeTime(item.lastUpdated)}
+ ${item.lastUpdated.toLocaleString()}`, + html`${item.lastUsed + ? html`
${getRelativeTime(item.lastUsed)}
+ ${item.lastUsed.toLocaleString()}` + : html`-`}`, ]; } } diff --git a/web/src/user/user-settings/mfa/MFADevicesPage.ts b/web/src/user/user-settings/mfa/MFADevicesPage.ts index 9bf3692344..707197b3d2 100644 --- a/web/src/user/user-settings/mfa/MFADevicesPage.ts +++ b/web/src/user/user-settings/mfa/MFADevicesPage.ts @@ -1,5 +1,6 @@ import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { deviceTypeName } from "@goauthentik/common/labels"; +import { getRelativeTime } from "@goauthentik/common/utils"; import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/TokenCopyButton"; @@ -48,6 +49,8 @@ export class MFADevicesPage extends Table { return [ msg("Name"), msg("Type"), + msg("Created at"), + msg("Last used at"), "" ].map((th) => new TableColumn(th, "")); } @@ -122,6 +125,12 @@ export class MFADevicesPage extends Table { return [ html`${item.name}`, html`${deviceTypeName(item)}`, + html`
${getRelativeTime(item.created)}
+ ${item.created.toLocaleString()}`, + html`${item.lastUsed + ? html`
${getRelativeTime(item.lastUsed)}
+ ${item.lastUsed.toLocaleString()}` + : html`-`}`, html` ${msg("Update")}