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 <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* also show for users

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* set allow_null

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Marc 'risson' Schmitt
2024-08-07 14:09:49 +02:00
committed by GitHub
parent 340106594e
commit d8c3b8bad2
11 changed files with 209 additions and 5 deletions

View File

@ -2,7 +2,13 @@
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema 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.permissions import IsAdminUser, IsAuthenticated
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
@ -20,6 +26,9 @@ class DeviceSerializer(MetaNameSerializer):
name = CharField() name = CharField()
type = SerializerMethodField() type = SerializerMethodField()
confirmed = BooleanField() 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: def get_type(self, instance: Device) -> str:
"""Get type of device""" """Get type of device"""

View File

@ -1,7 +1,12 @@
"""Authenticator devices helpers""" """Authenticator devices helpers"""
from typing import TYPE_CHECKING
from django.db import transaction from django.db import transaction
if TYPE_CHECKING:
from authentik.core.models import User
def verify_token(user, device_id, token): def verify_token(user, device_id, token):
""" """
@ -63,7 +68,7 @@ def match_token(user, token):
return device 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. Return an iterable of all devices registered to the given user.

View File

@ -9,6 +9,7 @@ from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from authentik.core.models import User from authentik.core.models import User
from authentik.lib.models import CreatedUpdatedModel
from authentik.stages.authenticator.util import random_number_token from authentik.stages.authenticator.util import random_number_token
@ -18,7 +19,7 @@ class DeviceManager(models.Manager):
``Device.objects``. ``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 Returns a queryset for all devices of this class that belong to the
given user. given user.
@ -37,7 +38,7 @@ class DeviceManager(models.Manager):
return devices return devices
class Device(models.Model): class Device(CreatedUpdatedModel):
""" """
Abstract base model for a :term:`device` attached to a user. Plugins must Abstract base model for a :term:`device` attached to a user. Plugins must
subclass this to define their OTP models. 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?") confirmed = models.BooleanField(default=True, help_text="Is this device ready for use?")
last_used = models.DateTimeField(null=True)
objects = DeviceManager() objects = DeviceManager()
class Meta: class Meta:

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -36459,8 +36459,24 @@ components:
readOnly: true readOnly: true
confirmed: confirmed:
type: boolean 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: required:
- confirmed - confirmed
- created
- last_updated
- last_used
- meta_model_name - meta_model_name
- name - name
- pk - pk

View File

@ -1,5 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { deviceTypeName } from "@goauthentik/common/labels"; import { deviceTypeName } from "@goauthentik/common/labels";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/DeleteBulkForm"; import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table"; import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table"; import { Table, TableColumn } from "@goauthentik/elements/table/Table";
@ -44,7 +45,10 @@ export class UserDeviceTable extends Table<Device> {
return [ return [
msg("Name"), msg("Name"),
msg("Type"), msg("Type"),
msg("Confirmed") msg("Confirmed"),
msg("Created at"),
msg("Last updated at"),
msg("Last used at"),
].map((th) => new TableColumn(th, "")); ].map((th) => new TableColumn(th, ""));
} }
@ -98,6 +102,14 @@ export class UserDeviceTable extends Table<Device> {
html`${item.name}`, html`${item.name}`,
html`${deviceTypeName(item)}`, html`${deviceTypeName(item)}`,
html`${item.confirmed ? msg("Yes") : msg("No")}`, html`${item.confirmed ? msg("Yes") : msg("No")}`,
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html`<div>${getRelativeTime(item.lastUpdated)}</div>
<small>${item.lastUpdated.toLocaleString()}</small>`,
html`${item.lastUsed
? html`<div>${getRelativeTime(item.lastUsed)}</div>
<small>${item.lastUsed.toLocaleString()}</small>`
: html`-`}`,
]; ];
} }
} }

View File

@ -1,5 +1,6 @@
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { deviceTypeName } from "@goauthentik/common/labels"; import { deviceTypeName } from "@goauthentik/common/labels";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/elements/buttons/Dropdown"; import "@goauthentik/elements/buttons/Dropdown";
import "@goauthentik/elements/buttons/ModalButton"; import "@goauthentik/elements/buttons/ModalButton";
import "@goauthentik/elements/buttons/TokenCopyButton"; import "@goauthentik/elements/buttons/TokenCopyButton";
@ -48,6 +49,8 @@ export class MFADevicesPage extends Table<Device> {
return [ return [
msg("Name"), msg("Name"),
msg("Type"), msg("Type"),
msg("Created at"),
msg("Last used at"),
"" ""
].map((th) => new TableColumn(th, "")); ].map((th) => new TableColumn(th, ""));
} }
@ -122,6 +125,12 @@ export class MFADevicesPage extends Table<Device> {
return [ return [
html`${item.name}`, html`${item.name}`,
html`${deviceTypeName(item)}`, html`${deviceTypeName(item)}`,
html`<div>${getRelativeTime(item.created)}</div>
<small>${item.created.toLocaleString()}</small>`,
html`${item.lastUsed
? html`<div>${getRelativeTime(item.lastUsed)}</div>
<small>${item.lastUsed.toLocaleString()}</small>`
: html`-`}`,
html` html`
<ak-forms-modal> <ak-forms-modal>
<span slot="submit">${msg("Update")}</span> <span slot="submit">${msg("Update")}</span>