endpoints/connectors: add ability to enroll a device as ephemeral

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer
2026-02-20 19:00:43 +01:00
parent 934f783bc7
commit cbdd210b03
7 changed files with 74 additions and 8 deletions

View File

@@ -75,6 +75,7 @@ class EnrollSerializer(PassiveSerializer):
device_serial = CharField(required=True)
device_name = CharField(required=True)
ephemeral = BooleanField(default=False)
class AgentTokenResponseSerializer(PassiveSerializer):

View File

@@ -40,15 +40,16 @@ from authentik.endpoints.models import Device
from authentik.events.models import Event, EventAction
from authentik.flows.planner import PLAN_CONTEXT_DEVICE
from authentik.lib.utils.reflection import ConditionalInheritance
from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
class AgentConnectorSerializer(ConnectorSerializer):
class Meta(ConnectorSerializer.Meta):
model = AgentConnector
fields = ConnectorSerializer.Meta.fields + [
"snapshot_expiry",
"ephemeral_device_expiry",
"auth_session_duration",
"auth_terminate_session_on_expiry",
"refresh_interval",
@@ -63,7 +64,6 @@ class AgentConnectorSerializer(ConnectorSerializer):
class MDMConfigSerializer(PassiveSerializer):
platform = ChoiceField(choices=OSFamily.choices)
enrollment_token = PrimaryKeyRelatedField(queryset=EnrollmentToken.objects.all())
@@ -87,7 +87,6 @@ class AgentConnectorViewSet(
UsedByMixin,
ModelViewSet,
):
queryset = AgentConnector.objects.all()
serializer_class = AgentConnectorSerializer
search_fields = ["name"]
@@ -124,13 +123,18 @@ class AgentConnectorViewSet(
token: EnrollmentToken = request.auth
data = EnrollSerializer(data=request.data)
data.is_valid(raise_exception=True)
defaults = {
"name": data.validated_data["device_name"],
"expiring": False,
"access_group": token.device_group,
}
if data.validated_data["ephemeral"]:
connector: AgentConnector = token.connector
defaults["expiring"] = True
defaults["expires"] = now() + timedelta_from_string(connector.ephemeral_device_expiry)
device, _ = Device.objects.get_or_create(
identifier=data.validated_data["device_serial"],
defaults={
"name": data.validated_data["device_name"],
"expiring": False,
"access_group": token.device_group,
},
defaults=defaults,
)
connection, _ = AgentDeviceConnection.objects.update_or_create(
device=device,
@@ -170,6 +174,11 @@ class AgentConnectorViewSet(
data.is_valid(raise_exception=True)
connection: AgentDeviceConnection = token.device
connection.create_snapshot(data.validated_data)
device = connection.device
if device.expiring:
connector = AgentConnector.objects.get(pk=connection.connector.pk)
device.expires = now() + timedelta_from_string(connector.ephemeral_device_expiry)
device.save()
return Response(status=204)
@extend_schema(

View File

@@ -0,0 +1,24 @@
# Generated by Django 5.2.11 on 2026-02-20 17:48
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_endpoints_connectors_agent",
"0004_agentconnector_challenge_idle_timeout_and_more",
),
]
operations = [
migrations.AddField(
model_name="agentconnector",
name="ephemeral_device_expiry",
field=models.TextField(
default="hours=2", validators=[authentik.lib.utils.time.timedelta_string_validator]
),
),
]

View File

@@ -32,6 +32,10 @@ class AgentConnector(Connector):
validators=[timedelta_string_validator],
)
ephemeral_device_expiry = models.TextField(
default="hours=2", validators=[timedelta_string_validator]
)
auth_session_duration = models.TextField(
default="hours=8", validators=[timedelta_string_validator]
)

View File

@@ -58,6 +58,18 @@ class TestAgentAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_enroll_ephemeral(self):
identifier = generate_id()
response = self.client.post(
reverse("authentik_api:agentconnector-enroll"),
data={"device_serial": identifier, "device_name": "bar", "ephemeral": True},
HTTP_AUTHORIZATION=f"Bearer {self.token.key}",
)
self.assertEqual(response.status_code, 200)
device = Device.objects.filter(identifier=identifier).first()
self.assertIsNotNone(device)
self.assertTrue(device.expiring)
def test_enroll_token_delete(self):
response = self.client.post(
reverse("authentik_api:agentconnector-enroll"),

View File

@@ -6304,6 +6304,11 @@
"minLength": 1,
"title": "Snapshot expiry"
},
"ephemeral_device_expiry": {
"type": "string",
"minLength": 1,
"title": "Ephemeral device expiry"
},
"auth_session_duration": {
"type": "string",
"minLength": 1,

View File

@@ -33843,6 +33843,8 @@ components:
readOnly: true
snapshot_expiry:
type: string
ephemeral_device_expiry:
type: string
auth_session_duration:
type: string
auth_terminate_session_on_expiry:
@@ -33893,6 +33895,9 @@ components:
snapshot_expiry:
type: string
minLength: 1
ephemeral_device_expiry:
type: string
minLength: 1
auth_session_duration:
type: string
minLength: 1
@@ -38296,6 +38301,9 @@ components:
device_name:
type: string
minLength: 1
ephemeral:
type: boolean
default: false
required:
- device_name
- device_serial
@@ -47112,6 +47120,9 @@ components:
snapshot_expiry:
type: string
minLength: 1
ephemeral_device_expiry:
type: string
minLength: 1
auth_session_duration:
type: string
minLength: 1