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.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"""

View File

@ -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.

View File

@ -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:

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
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

View File

@ -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<Device> {
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<Device> {
html`${item.name}`,
html`${deviceTypeName(item)}`,
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 { 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<Device> {
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<Device> {
return [
html`${item.name}`,
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`
<ak-forms-modal>
<span slot="submit">${msg("Update")}</span>