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 AllowAny, 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 class Meta(ConnectorSerializer.Meta): 48 model = AgentConnector 49 fields = ConnectorSerializer.Meta.fields + [ 50 "snapshot_expiry", 51 "auth_session_duration", 52 "auth_terminate_session_on_expiry", 53 "refresh_interval", 54 "authorization_flow", 55 "nss_uid_offset", 56 "nss_gid_offset", 57 "challenge_key", 58 "challenge_idle_timeout", 59 "challenge_trigger_check_in", 60 "jwt_federation_providers", 61 ] 62 63 64class MDMConfigSerializer(PassiveSerializer): 65 platform = ChoiceField(choices=OSFamily.choices) 66 enrollment_token = PrimaryKeyRelatedField( 67 queryset=EnrollmentToken.objects.including_expired().all() 68 ) 69 70 def validate_platform(self, platform: OSFamily) -> OSFamily: 71 if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]: 72 raise ValidationError(_("Selected platform not supported")) 73 return platform 74 75 def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken: 76 if token.is_expired: 77 raise ValidationError(_("Token is expired")) 78 if token.connector != self.context["connector"]: 79 raise ValidationError(_("Invalid token for connector")) 80 return token 81 82 83class AgentConnectorViewSet( 84 ConditionalInheritance( 85 "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin" 86 ), 87 UsedByMixin, 88 ModelViewSet, 89): 90 queryset = AgentConnector.objects.all() 91 serializer_class = AgentConnectorSerializer 92 search_fields = ["name"] 93 ordering = ["name"] 94 filterset_fields = ["name", "enabled"] 95 96 @extend_schema( 97 request=MDMConfigSerializer(), 98 responses=MDMConfigResponseSerializer(), 99 ) 100 @action(methods=["POST"], detail=True) 101 def mdm_config(self, request: Request, pk) -> Response: 102 """Generate configuration for MDM systems to deploy authentik Agent""" 103 connector = cast(AgentConnector, self.get_object()) 104 data = MDMConfigSerializer(data=request.data, context={"connector": connector}) 105 data.is_valid(raise_exception=True) 106 token = data.validated_data["enrollment_token"] 107 if not request.user.has_perm("view_enrollment_token_key", token): 108 raise PermissionDenied() 109 ctrl = connector.controller(connector) 110 payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token) 111 return Response(payload.validated_data) 112 113 @extend_schema( 114 request=EnrollSerializer(), 115 responses={200: AgentTokenResponseSerializer}, 116 ) 117 @action( 118 methods=["POST"], 119 detail=False, 120 authentication_classes=[AgentEnrollmentAuth], 121 # Permissions are handled via AgentEnrollmentAuth 122 permission_classes=[AllowAny], 123 ) 124 def enroll(self, request: Request): 125 token: EnrollmentToken = request.auth 126 data = EnrollSerializer(data=request.data) 127 data.is_valid(raise_exception=True) 128 device, _ = Device.objects.get_or_create( 129 identifier=data.validated_data["device_serial"], 130 defaults={ 131 "name": data.validated_data["device_name"], 132 "expiring": False, 133 "access_group": token.device_group, 134 }, 135 ) 136 connection, _ = AgentDeviceConnection.objects.update_or_create( 137 device=device, 138 connector=token.connector, 139 ) 140 DeviceToken.objects.including_expired().filter(device=connection).delete() 141 token = DeviceToken.objects.create(device=connection, expiring=False) 142 return Response( 143 { 144 "token": token.key, 145 "expires_in": 0, 146 } 147 ) 148 149 @extend_schema( 150 request=OpenApiTypes.NONE, 151 responses=AgentConfigSerializer(), 152 ) 153 @action( 154 methods=["GET"], 155 detail=False, 156 authentication_classes=[AgentAuth], 157 # Permissions are handled via AgentAuth 158 permission_classes=[AllowAny], 159 ) 160 def agent_config(self, request: Request): 161 token: DeviceToken = request.auth 162 connector: AgentConnector = token.device.connector.agentconnector 163 return Response( 164 AgentConfigSerializer( 165 connector, context={"request": request, "device": token.device.device} 166 ).data 167 ) 168 169 @extend_schema( 170 request=DeviceFacts(), 171 responses={204: OpenApiResponse(description="Successfully checked in")}, 172 ) 173 @action( 174 methods=["POST"], 175 detail=False, 176 authentication_classes=[AgentAuth], 177 # Permissions are handled via AgentAuth 178 permission_classes=[AllowAny], 179 ) 180 def check_in(self, request: Request): 181 token: DeviceToken = request.auth 182 data = DeviceFacts(data=request.data) 183 data.is_valid(raise_exception=True) 184 connection: AgentDeviceConnection = token.device 185 connection.create_snapshot(data.validated_data) 186 return Response(status=204) 187 188 @extend_schema( 189 request=OpenApiTypes.NONE, 190 parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)], 191 responses={ 192 200: AgentTokenResponseSerializer(), 193 404: OpenApiResponse(description="Device not found"), 194 }, 195 ) 196 @action( 197 methods=["POST"], 198 detail=False, 199 pagination_class=None, 200 filter_backends=[], 201 permission_classes=[IsAuthenticated], 202 authentication_classes=[DeviceAuthFedAuthentication], 203 ) 204 def auth_fed(self, request: Request) -> Response: 205 federated_token, device, connector = request.auth 206 207 policy_result = check_device_policies(device, federated_token.user, request._request) 208 if not policy_result.passing: 209 raise ValidationError( 210 {"policy_result": "Policy denied access", "policy_messages": policy_result.messages} 211 ) 212 213 token, exp = agent_auth_issue_token(device, connector, federated_token.user) 214 rel_exp = int((exp - now()).total_seconds()) 215 Event.new( 216 EventAction.LOGIN, 217 **{ 218 PLAN_CONTEXT_METHOD: "jwt", 219 PLAN_CONTEXT_METHOD_ARGS: { 220 "jwt": federated_token, 221 "provider": federated_token.provider, 222 }, 223 PLAN_CONTEXT_DEVICE: device, 224 }, 225 ).from_http(request, user=federated_token.user) 226 return Response({"token": token, "expires_in": rel_exp})
47class AgentConnectorSerializer(ConnectorSerializer): 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 ]
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
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 ]
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']
65class MDMConfigSerializer(PassiveSerializer): 66 platform = ChoiceField(choices=OSFamily.choices) 67 enrollment_token = PrimaryKeyRelatedField( 68 queryset=EnrollmentToken.objects.including_expired().all() 69 ) 70 71 def validate_platform(self, platform: OSFamily) -> OSFamily: 72 if platform not in [OSFamily.iOS, OSFamily.macOS, OSFamily.windows]: 73 raise ValidationError(_("Selected platform not supported")) 74 return platform 75 76 def validate_enrollment_token(self, token: EnrollmentToken) -> EnrollmentToken: 77 if token.is_expired: 78 raise ValidationError(_("Token is expired")) 79 if token.connector != self.context["connector"]: 80 raise ValidationError(_("Invalid token for connector")) 81 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):
84class AgentConnectorViewSet( 85 ConditionalInheritance( 86 "authentik.enterprise.endpoints.connectors.agent.api.connectors.AgentConnectorViewSetMixin" 87 ), 88 UsedByMixin, 89 ModelViewSet, 90): 91 queryset = AgentConnector.objects.all() 92 serializer_class = AgentConnectorSerializer 93 search_fields = ["name"] 94 ordering = ["name"] 95 filterset_fields = ["name", "enabled"] 96 97 @extend_schema( 98 request=MDMConfigSerializer(), 99 responses=MDMConfigResponseSerializer(), 100 ) 101 @action(methods=["POST"], detail=True) 102 def mdm_config(self, request: Request, pk) -> Response: 103 """Generate configuration for MDM systems to deploy authentik Agent""" 104 connector = cast(AgentConnector, self.get_object()) 105 data = MDMConfigSerializer(data=request.data, context={"connector": connector}) 106 data.is_valid(raise_exception=True) 107 token = data.validated_data["enrollment_token"] 108 if not request.user.has_perm("view_enrollment_token_key", token): 109 raise PermissionDenied() 110 ctrl = connector.controller(connector) 111 payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token) 112 return Response(payload.validated_data) 113 114 @extend_schema( 115 request=EnrollSerializer(), 116 responses={200: AgentTokenResponseSerializer}, 117 ) 118 @action( 119 methods=["POST"], 120 detail=False, 121 authentication_classes=[AgentEnrollmentAuth], 122 # Permissions are handled via AgentEnrollmentAuth 123 permission_classes=[AllowAny], 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( 155 methods=["GET"], 156 detail=False, 157 authentication_classes=[AgentAuth], 158 # Permissions are handled via AgentAuth 159 permission_classes=[AllowAny], 160 ) 161 def agent_config(self, request: Request): 162 token: DeviceToken = request.auth 163 connector: AgentConnector = token.device.connector.agentconnector 164 return Response( 165 AgentConfigSerializer( 166 connector, context={"request": request, "device": token.device.device} 167 ).data 168 ) 169 170 @extend_schema( 171 request=DeviceFacts(), 172 responses={204: OpenApiResponse(description="Successfully checked in")}, 173 ) 174 @action( 175 methods=["POST"], 176 detail=False, 177 authentication_classes=[AgentAuth], 178 # Permissions are handled via AgentAuth 179 permission_classes=[AllowAny], 180 ) 181 def check_in(self, request: Request): 182 token: DeviceToken = request.auth 183 data = DeviceFacts(data=request.data) 184 data.is_valid(raise_exception=True) 185 connection: AgentDeviceConnection = token.device 186 connection.create_snapshot(data.validated_data) 187 return Response(status=204) 188 189 @extend_schema( 190 request=OpenApiTypes.NONE, 191 parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)], 192 responses={ 193 200: AgentTokenResponseSerializer(), 194 404: OpenApiResponse(description="Device not found"), 195 }, 196 ) 197 @action( 198 methods=["POST"], 199 detail=False, 200 pagination_class=None, 201 filter_backends=[], 202 permission_classes=[IsAuthenticated], 203 authentication_classes=[DeviceAuthFedAuthentication], 204 ) 205 def auth_fed(self, request: Request) -> Response: 206 federated_token, device, connector = request.auth 207 208 policy_result = check_device_policies(device, federated_token.user, request._request) 209 if not policy_result.passing: 210 raise ValidationError( 211 {"policy_result": "Policy denied access", "policy_messages": policy_result.messages} 212 ) 213 214 token, exp = agent_auth_issue_token(device, connector, federated_token.user) 215 rel_exp = int((exp - now()).total_seconds()) 216 Event.new( 217 EventAction.LOGIN, 218 **{ 219 PLAN_CONTEXT_METHOD: "jwt", 220 PLAN_CONTEXT_METHOD_ARGS: { 221 "jwt": federated_token, 222 "provider": federated_token.provider, 223 }, 224 PLAN_CONTEXT_DEVICE: device, 225 }, 226 ).from_http(request, user=federated_token.user) 227 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:
97 @extend_schema( 98 request=MDMConfigSerializer(), 99 responses=MDMConfigResponseSerializer(), 100 ) 101 @action(methods=["POST"], detail=True) 102 def mdm_config(self, request: Request, pk) -> Response: 103 """Generate configuration for MDM systems to deploy authentik Agent""" 104 connector = cast(AgentConnector, self.get_object()) 105 data = MDMConfigSerializer(data=request.data, context={"connector": connector}) 106 data.is_valid(raise_exception=True) 107 token = data.validated_data["enrollment_token"] 108 if not request.user.has_perm("view_enrollment_token_key", token): 109 raise PermissionDenied() 110 ctrl = connector.controller(connector) 111 payload = ctrl.generate_mdm_config(data.validated_data["platform"], request, token) 112 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], permission_classes=[AllowAny])
def
enroll(self, request: rest_framework.request.Request):
114 @extend_schema( 115 request=EnrollSerializer(), 116 responses={200: AgentTokenResponseSerializer}, 117 ) 118 @action( 119 methods=["POST"], 120 detail=False, 121 authentication_classes=[AgentEnrollmentAuth], 122 # Permissions are handled via AgentEnrollmentAuth 123 permission_classes=[AllowAny], 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 )
@extend_schema(request=OpenApiTypes.NONE, responses=AgentConfigSerializer())
@action(methods=['GET'], detail=False, authentication_classes=[AgentAuth], permission_classes=[AllowAny])
def
agent_config(self, request: rest_framework.request.Request):
150 @extend_schema( 151 request=OpenApiTypes.NONE, 152 responses=AgentConfigSerializer(), 153 ) 154 @action( 155 methods=["GET"], 156 detail=False, 157 authentication_classes=[AgentAuth], 158 # Permissions are handled via AgentAuth 159 permission_classes=[AllowAny], 160 ) 161 def agent_config(self, request: Request): 162 token: DeviceToken = request.auth 163 connector: AgentConnector = token.device.connector.agentconnector 164 return Response( 165 AgentConfigSerializer( 166 connector, context={"request": request, "device": token.device.device} 167 ).data 168 )
@extend_schema(request=DeviceFacts(), responses={204: OpenApiResponse(description='Successfully checked in')})
@action(methods=['POST'], detail=False, authentication_classes=[AgentAuth], permission_classes=[AllowAny])
def
check_in(self, request: rest_framework.request.Request):
170 @extend_schema( 171 request=DeviceFacts(), 172 responses={204: OpenApiResponse(description="Successfully checked in")}, 173 ) 174 @action( 175 methods=["POST"], 176 detail=False, 177 authentication_classes=[AgentAuth], 178 # Permissions are handled via AgentAuth 179 permission_classes=[AllowAny], 180 ) 181 def check_in(self, request: Request): 182 token: DeviceToken = request.auth 183 data = DeviceFacts(data=request.data) 184 data.is_valid(raise_exception=True) 185 connection: AgentDeviceConnection = token.device 186 connection.create_snapshot(data.validated_data) 187 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:
189 @extend_schema( 190 request=OpenApiTypes.NONE, 191 parameters=[OpenApiParameter("device", OpenApiTypes.STR, location="query", required=True)], 192 responses={ 193 200: AgentTokenResponseSerializer(), 194 404: OpenApiResponse(description="Device not found"), 195 }, 196 ) 197 @action( 198 methods=["POST"], 199 detail=False, 200 pagination_class=None, 201 filter_backends=[], 202 permission_classes=[IsAuthenticated], 203 authentication_classes=[DeviceAuthFedAuthentication], 204 ) 205 def auth_fed(self, request: Request) -> Response: 206 federated_token, device, connector = request.auth 207 208 policy_result = check_device_policies(device, federated_token.user, request._request) 209 if not policy_result.passing: 210 raise ValidationError( 211 {"policy_result": "Policy denied access", "policy_messages": policy_result.messages} 212 ) 213 214 token, exp = agent_auth_issue_token(device, connector, federated_token.user) 215 rel_exp = int((exp - now()).total_seconds()) 216 Event.new( 217 EventAction.LOGIN, 218 **{ 219 PLAN_CONTEXT_METHOD: "jwt", 220 PLAN_CONTEXT_METHOD_ARGS: { 221 "jwt": federated_token, 222 "provider": federated_token.provider, 223 }, 224 PLAN_CONTEXT_DEVICE: device, 225 }, 226 ).from_http(request, user=federated_token.user) 227 return Response({"token": token, "expires_in": rel_exp})