authentik.endpoints.connectors.agent.api.connectors
1from typing import cast 2 3from django.utils.timezone import now 4from django.utils.translation import gettext_lazy as _ 5from drf_spectacular.types import OpenApiTypes 6from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema 7from rest_framework.decorators import action 8from rest_framework.exceptions import PermissionDenied, ValidationError 9from rest_framework.fields import ChoiceField 10from rest_framework.permissions import IsAuthenticated 11from rest_framework.relations import PrimaryKeyRelatedField 12from rest_framework.request import Request 13from rest_framework.response import Response 14from rest_framework.viewsets import ModelViewSet 15 16from authentik.core.api.used_by import UsedByMixin 17from authentik.core.api.utils import PassiveSerializer 18from authentik.endpoints.api.connectors import ConnectorSerializer 19from authentik.endpoints.connectors.agent.api.agent import ( 20 AgentConfigSerializer, 21 AgentTokenResponseSerializer, 22 EnrollSerializer, 23) 24from authentik.endpoints.connectors.agent.auth import ( 25 AgentAuth, 26 AgentEnrollmentAuth, 27 DeviceAuthFedAuthentication, 28 agent_auth_issue_token, 29 check_device_policies, 30) 31from authentik.endpoints.connectors.agent.controller import MDMConfigResponseSerializer 32from authentik.endpoints.connectors.agent.models import ( 33 AgentConnector, 34 AgentDeviceConnection, 35 DeviceToken, 36 EnrollmentToken, 37) 38from authentik.endpoints.facts import DeviceFacts, OSFamily 39from authentik.endpoints.models import Device 40from authentik.events.models import Event, EventAction 41from authentik.flows.planner import PLAN_CONTEXT_DEVICE 42from authentik.lib.utils.reflection import ConditionalInheritance 43from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS 44 45 46class AgentConnectorSerializer(ConnectorSerializer): 47 48 class Meta(ConnectorSerializer.Meta): 49 model = AgentConnector 50 fields = ConnectorSerializer.Meta.fields + [ 51 "snapshot_expiry", 52 "auth_session_duration", 53 "auth_terminate_session_on_expiry", 54 "refresh_interval", 55 "authorization_flow", 56 "nss_uid_offset", 57 "nss_gid_offset", 58 "challenge_key", 59 "challenge_idle_timeout", 60 "challenge_trigger_check_in", 61 "jwt_federation_providers", 62 ] 63 64 65class MDMConfigSerializer(PassiveSerializer): 66 67 platform = ChoiceField(choices=OSFamily.choices) 68 enrollment_token = PrimaryKeyRelatedField( 69 queryset=EnrollmentToken.objects.including_expired().all() 70 ) 71 72 def validate_platform(self, platform: OSFamily) -> OSFamily: 73 if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]: 74 raise ValidationError(_("Selected platform not supported")) 75 return platform 76 77 def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken: 78 if token.is_expired: 79 raise ValidationError(_("Token is expired")) 80 if token.connector != self.context["connector"]: 81 raise ValidationError(_("Invalid token for connector")) 82 return token 83 84 85class AgentConnectorViewSet( 86 ConditionalInheritance( 87 "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin" 88 ), 89 UsedByMixin, 90 ModelViewSet, 91): 92 93 queryset = AgentConnector.objects.all() 94 serializer_class = AgentConnectorSerializer 95 search_fields = ["name"] 96 ordering = ["name"] 97 filterset_fields = ["name", "enabled"] 98 99 @extend_schema( 100 request=MDMConfigSerializer(), 101 responses=MDMConfigResponseSerializer(), 102 ) 103 @action(methods=["POST"], detail=True) 104 def mdm_config(self, request: Request, pk) -> Response: 105 """Generate configuration for MDM systems to deploy authentik Agent""" 106 connector = cast(AgentConnector, self.get_object()) 107 data = MDMConfigSerializer(data=request.data, context={"connector": connector}) 108 data.is_valid(raise_exception=True) 109 token = data.validated_data["enrollment_token"] 110 if not request.user.has_perm("view_enrollment_token_key", token): 111 raise PermissionDenied() 112 ctrl = connector.controller(connector) 113 payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token) 114 return Response(payload.validated_data) 115 116 @extend_schema( 117 request=EnrollSerializer(), 118 responses={200: AgentTokenResponseSerializer}, 119 ) 120 @action( 121 methods=["POST"], 122 detail=False, 123 authentication_classes=[AgentEnrollmentAuth], 124 ) 125 def enroll(self, request: Request): 126 token: EnrollmentToken = request.auth 127 data = EnrollSerializer(data=request.data) 128 data.is_valid(raise_exception=True) 129 device, _ = Device.objects.get_or_create( 130 identifier=data.validated_data["device_serial"], 131 defaults={ 132 "name": data.validated_data["device_name"], 133 "expiring": False, 134 "access_group": token.device_group, 135 }, 136 ) 137 connection, _ = AgentDeviceConnection.objects.update_or_create( 138 device=device, 139 connector=token.connector, 140 ) 141 DeviceToken.objects.including_expired().filter(device=connection).delete() 142 token = DeviceToken.objects.create(device=connection, expiring=False) 143 return Response( 144 { 145 "token": token.key, 146 "expires_in": 0, 147 } 148 ) 149 150 @extend_schema( 151 request=OpenApiTypes.NONE, 152 responses=AgentConfigSerializer(), 153 ) 154 @action(methods=["GET"], detail=False, authentication_classes=[AgentAuth]) 155 def agent_config(self, request: Request): 156 token: DeviceToken = request.auth 157 connector: AgentConnector = token.device.connector.agentconnector 158 return Response( 159 AgentConfigSerializer( 160 connector, context={"request": request, "device": token.device.device} 161 ).data 162 ) 163 164 @extend_schema( 165 request=DeviceFacts(), 166 responses={204: OpenApiResponse(description="Successfully checked in")}, 167 ) 168 @action(methods=["POST"], detail=False, authentication_classes=[AgentAuth]) 169 def check_in(self, request: Request): 170 token: DeviceToken = request.auth 171 data = DeviceFacts(data=request.data) 172 data.is_valid(raise_exception=True) 173 connection: AgentDeviceConnection = token.device 174 connection.create_snapshot(data.validated_data) 175 return Response(status=204) 176 177 @extend_schema( 178 request=OpenApiTypes.NONE, 179 parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)], 180 responses={ 181 200: AgentTokenResponseSerializer(), 182 404: OpenApiResponse(description="Device not found"), 183 }, 184 ) 185 @action( 186 methods=["POST"], 187 detail=False, 188 pagination_class=None, 189 filter_backends=[], 190 permission_classes=[IsAuthenticated], 191 authentication_classes=[DeviceAuthFedAuthentication], 192 ) 193 def auth_fed(self, request: Request) -> Response: 194 federated_token, device, connector = request.auth 195 196 policy_result = check_device_policies(device, federated_token.user, request._request) 197 if not policy_result.passing: 198 raise ValidationError( 199 {"policy_result": "Policy denied access", "policy_messages": policy_result.messages} 200 ) 201 202 token, exp = agent_auth_issue_token(device, connector, federated_token.user) 203 rel_exp = int((exp - now()).total_seconds()) 204 Event.new( 205 EventAction.LOGIN, 206 **{ 207 PLAN_CONTEXT_METHOD: "jwt", 208 PLAN_CONTEXT_METHOD_ARGS: { 209 "jwt": federated_token, 210 "provider": federated_token.provider, 211 }, 212 PLAN_CONTEXT_DEVICE: device, 213 }, 214 ).from_http(request, user=federated_token.user) 215 return Response({"token": token, "expires_in": rel_exp})
47class AgentConnectorSerializer(ConnectorSerializer): 48 49 class Meta(ConnectorSerializer.Meta): 50 model = AgentConnector 51 fields = ConnectorSerializer.Meta.fields + [ 52 "snapshot_expiry", 53 "auth_session_duration", 54 "auth_terminate_session_on_expiry", 55 "refresh_interval", 56 "authorization_flow", 57 "nss_uid_offset", 58 "nss_gid_offset", 59 "challenge_key", 60 "challenge_idle_timeout", 61 "challenge_trigger_check_in", 62 "jwt_federation_providers", 63 ]
A ModelSerializer is just a regular Serializer, except that:
- A set of default fields are automatically populated.
- A set of default validators are automatically populated.
- Default
.create()and.update()implementations are provided.
The process of automatically determining a set of serializer fields based on the model fields is reasonably complex, but you almost certainly don't need to dig into the implementation.
If the ModelSerializer class doesn't generate the set of fields that
you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a Serializer class.
Inherited Members
49 class Meta(ConnectorSerializer.Meta): 50 model = AgentConnector 51 fields = ConnectorSerializer.Meta.fields + [ 52 "snapshot_expiry", 53 "auth_session_duration", 54 "auth_terminate_session_on_expiry", 55 "refresh_interval", 56 "authorization_flow", 57 "nss_uid_offset", 58 "nss_gid_offset", 59 "challenge_key", 60 "challenge_idle_timeout", 61 "challenge_trigger_check_in", 62 "jwt_federation_providers", 63 ]
model =
<class 'authentik.endpoints.connectors.agent.models.AgentConnector'>
fields =
['connector_uuid', 'name', 'enabled', 'component', 'verbose_name', 'verbose_name_plural', 'meta_model_name', 'snapshot_expiry', 'auth_session_duration', 'auth_terminate_session_on_expiry', 'refresh_interval', 'authorization_flow', 'nss_uid_offset', 'nss_gid_offset', 'challenge_key', 'challenge_idle_timeout', 'challenge_trigger_check_in', 'jwt_federation_providers']
66class MDMConfigSerializer(PassiveSerializer): 67 68 platform = ChoiceField(choices=OSFamily.choices) 69 enrollment_token = PrimaryKeyRelatedField( 70 queryset=EnrollmentToken.objects.including_expired().all() 71 ) 72 73 def validate_platform(self, platform: OSFamily) -> OSFamily: 74 if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]: 75 raise ValidationError(_("Selected platform not supported")) 76 return platform 77 78 def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken: 79 if token.is_expired: 80 raise ValidationError(_("Token is expired")) 81 if token.connector != self.context["connector"]: 82 raise ValidationError(_("Invalid token for connector")) 83 return token
Base serializer class which doesn't implement create/update methods
def
validate_platform( self, platform: authentik.endpoints.facts.OSFamily) -> authentik.endpoints.facts.OSFamily:
def
validate_enrollment_token( self, token: authentik.endpoints.connectors.agent.models.EnrollmentToken) -> authentik.endpoints.connectors.agent.models.EnrollmentToken:
Inherited Members
class
AgentConnectorViewSet(authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin, authentik.core.api.used_by.UsedByMixin, rest_framework.viewsets.ModelViewSet):
86class AgentConnectorViewSet( 87 ConditionalInheritance( 88 "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin" 89 ), 90 UsedByMixin, 91 ModelViewSet, 92): 93 94 queryset = AgentConnector.objects.all() 95 serializer_class = AgentConnectorSerializer 96 search_fields = ["name"] 97 ordering = ["name"] 98 filterset_fields = ["name", "enabled"] 99 100 @extend_schema( 101 request=MDMConfigSerializer(), 102 responses=MDMConfigResponseSerializer(), 103 ) 104 @action(methods=["POST"], detail=True) 105 def mdm_config(self, request: Request, pk) -> Response: 106 """Generate configuration for MDM systems to deploy authentik Agent""" 107 connector = cast(AgentConnector, self.get_object()) 108 data = MDMConfigSerializer(data=request.data, context={"connector": connector}) 109 data.is_valid(raise_exception=True) 110 token = data.validated_data["enrollment_token"] 111 if not request.user.has_perm("view_enrollment_token_key", token): 112 raise PermissionDenied() 113 ctrl = connector.controller(connector) 114 payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token) 115 return Response(payload.validated_data) 116 117 @extend_schema( 118 request=EnrollSerializer(), 119 responses={200: AgentTokenResponseSerializer}, 120 ) 121 @action( 122 methods=["POST"], 123 detail=False, 124 authentication_classes=[AgentEnrollmentAuth], 125 ) 126 def enroll(self, request: Request): 127 token: EnrollmentToken = request.auth 128 data = EnrollSerializer(data=request.data) 129 data.is_valid(raise_exception=True) 130 device, _ = Device.objects.get_or_create( 131 identifier=data.validated_data["device_serial"], 132 defaults={ 133 "name": data.validated_data["device_name"], 134 "expiring": False, 135 "access_group": token.device_group, 136 }, 137 ) 138 connection, _ = AgentDeviceConnection.objects.update_or_create( 139 device=device, 140 connector=token.connector, 141 ) 142 DeviceToken.objects.including_expired().filter(device=connection).delete() 143 token = DeviceToken.objects.create(device=connection, expiring=False) 144 return Response( 145 { 146 "token": token.key, 147 "expires_in": 0, 148 } 149 ) 150 151 @extend_schema( 152 request=OpenApiTypes.NONE, 153 responses=AgentConfigSerializer(), 154 ) 155 @action(methods=["GET"], detail=False, authentication_classes=[AgentAuth]) 156 def agent_config(self, request: Request): 157 token: DeviceToken = request.auth 158 connector: AgentConnector = token.device.connector.agentconnector 159 return Response( 160 AgentConfigSerializer( 161 connector, context={"request": request, "device": token.device.device} 162 ).data 163 ) 164 165 @extend_schema( 166 request=DeviceFacts(), 167 responses={204: OpenApiResponse(description="Successfully checked in")}, 168 ) 169 @action(methods=["POST"], detail=False, authentication_classes=[AgentAuth]) 170 def check_in(self, request: Request): 171 token: DeviceToken = request.auth 172 data = DeviceFacts(data=request.data) 173 data.is_valid(raise_exception=True) 174 connection: AgentDeviceConnection = token.device 175 connection.create_snapshot(data.validated_data) 176 return Response(status=204) 177 178 @extend_schema( 179 request=OpenApiTypes.NONE, 180 parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)], 181 responses={ 182 200: AgentTokenResponseSerializer(), 183 404: OpenApiResponse(description="Device not found"), 184 }, 185 ) 186 @action( 187 methods=["POST"], 188 detail=False, 189 pagination_class=None, 190 filter_backends=[], 191 permission_classes=[IsAuthenticated], 192 authentication_classes=[DeviceAuthFedAuthentication], 193 ) 194 def auth_fed(self, request: Request) -> Response: 195 federated_token, device, connector = request.auth 196 197 policy_result = check_device_policies(device, federated_token.user, request._request) 198 if not policy_result.passing: 199 raise ValidationError( 200 {"policy_result": "Policy denied access", "policy_messages": policy_result.messages} 201 ) 202 203 token, exp = agent_auth_issue_token(device, connector, federated_token.user) 204 rel_exp = int((exp - now()).total_seconds()) 205 Event.new( 206 EventAction.LOGIN, 207 **{ 208 PLAN_CONTEXT_METHOD: "jwt", 209 PLAN_CONTEXT_METHOD_ARGS: { 210 "jwt": federated_token, 211 "provider": federated_token.provider, 212 }, 213 PLAN_CONTEXT_DEVICE: device, 214 }, 215 ).from_http(request, user=federated_token.user) 216 return Response({"token": token, "expires_in": rel_exp})
Mixin to add a used_by endpoint to return a list of all objects using this object
serializer_class =
<class 'AgentConnectorSerializer'>
@extend_schema(request=MDMConfigSerializer(), responses=MDMConfigResponseSerializer())
@action(methods=['POST'], detail=True)
def
mdm_config( self, request: rest_framework.request.Request, pk) -> rest_framework.response.Response:
100 @extend_schema( 101 request=MDMConfigSerializer(), 102 responses=MDMConfigResponseSerializer(), 103 ) 104 @action(methods=["POST"], detail=True) 105 def mdm_config(self, request: Request, pk) -> Response: 106 """Generate configuration for MDM systems to deploy authentik Agent""" 107 connector = cast(AgentConnector, self.get_object()) 108 data = MDMConfigSerializer(data=request.data, context={"connector": connector}) 109 data.is_valid(raise_exception=True) 110 token = data.validated_data["enrollment_token"] 111 if not request.user.has_perm("view_enrollment_token_key", token): 112 raise PermissionDenied() 113 ctrl = connector.controller(connector) 114 payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token) 115 return Response(payload.validated_data)
Generate configuration for MDM systems to deploy authentik Agent
@extend_schema(request=EnrollSerializer(), responses={200: AgentTokenResponseSerializer})
@action(methods=['POST'], detail=False, authentication_classes=[AgentEnrollmentAuth])
def
enroll(self, request: rest_framework.request.Request):
117 @extend_schema( 118 request=EnrollSerializer(), 119 responses={200: AgentTokenResponseSerializer}, 120 ) 121 @action( 122 methods=["POST"], 123 detail=False, 124 authentication_classes=[AgentEnrollmentAuth], 125 ) 126 def enroll(self, request: Request): 127 token: EnrollmentToken = request.auth 128 data = EnrollSerializer(data=request.data) 129 data.is_valid(raise_exception=True) 130 device, _ = Device.objects.get_or_create( 131 identifier=data.validated_data["device_serial"], 132 defaults={ 133 "name": data.validated_data["device_name"], 134 "expiring": False, 135 "access_group": token.device_group, 136 }, 137 ) 138 connection, _ = AgentDeviceConnection.objects.update_or_create( 139 device=device, 140 connector=token.connector, 141 ) 142 DeviceToken.objects.including_expired().filter(device=connection).delete() 143 token = DeviceToken.objects.create(device=connection, expiring=False) 144 return Response( 145 { 146 "token": token.key, 147 "expires_in": 0, 148 } 149 )
@extend_schema(request=OpenApiTypes.NONE, responses=AgentConfigSerializer())
@action(methods=['GET'], detail=False, authentication_classes=[AgentAuth])
def
agent_config(self, request: rest_framework.request.Request):
151 @extend_schema( 152 request=OpenApiTypes.NONE, 153 responses=AgentConfigSerializer(), 154 ) 155 @action(methods=["GET"], detail=False, authentication_classes=[AgentAuth]) 156 def agent_config(self, request: Request): 157 token: DeviceToken = request.auth 158 connector: AgentConnector = token.device.connector.agentconnector 159 return Response( 160 AgentConfigSerializer( 161 connector, context={"request": request, "device": token.device.device} 162 ).data 163 )
@extend_schema(request=DeviceFacts(), responses={204: OpenApiResponse(description='Successfully checked in')})
@action(methods=['POST'], detail=False, authentication_classes=[AgentAuth])
def
check_in(self, request: rest_framework.request.Request):
165 @extend_schema( 166 request=DeviceFacts(), 167 responses={204: OpenApiResponse(description="Successfully checked in")}, 168 ) 169 @action(methods=["POST"], detail=False, authentication_classes=[AgentAuth]) 170 def check_in(self, request: Request): 171 token: DeviceToken = request.auth 172 data = DeviceFacts(data=request.data) 173 data.is_valid(raise_exception=True) 174 connection: AgentDeviceConnection = token.device 175 connection.create_snapshot(data.validated_data) 176 return Response(status=204)
@extend_schema(request=OpenApiTypes.NONE, parameters=[OpenApiParameter('device', OpenApiTypes.STR, location='query', required=True)], responses={200: AgentTokenResponseSerializer(), 404: OpenApiResponse(description='Device not found')})
@action(methods=['POST'], detail=False, pagination_class=None, filter_backends=[], permission_classes=[IsAuthenticated], authentication_classes=[DeviceAuthFedAuthentication])
def
auth_fed( self, request: rest_framework.request.Request) -> rest_framework.response.Response:
178 @extend_schema( 179 request=OpenApiTypes.NONE, 180 parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)], 181 responses={ 182 200: AgentTokenResponseSerializer(), 183 404: OpenApiResponse(description="Device not found"), 184 }, 185 ) 186 @action( 187 methods=["POST"], 188 detail=False, 189 pagination_class=None, 190 filter_backends=[], 191 permission_classes=[IsAuthenticated], 192 authentication_classes=[DeviceAuthFedAuthentication], 193 ) 194 def auth_fed(self, request: Request) -> Response: 195 federated_token, device, connector = request.auth 196 197 policy_result = check_device_policies(device, federated_token.user, request._request) 198 if not policy_result.passing: 199 raise ValidationError( 200 {"policy_result": "Policy denied access", "policy_messages": policy_result.messages} 201 ) 202 203 token, exp = agent_auth_issue_token(device, connector, federated_token.user) 204 rel_exp = int((exp - now()).total_seconds()) 205 Event.new( 206 EventAction.LOGIN, 207 **{ 208 PLAN_CONTEXT_METHOD: "jwt", 209 PLAN_CONTEXT_METHOD_ARGS: { 210 "jwt": federated_token, 211 "provider": federated_token.provider, 212 }, 213 PLAN_CONTEXT_DEVICE: device, 214 }, 215 ).from_http(request, user=federated_token.user) 216 return Response({"token": token, "expires_in": rel_exp})