authentik.stages.invitation.api

Invitation Stage API Views

  1"""Invitation Stage API Views"""
  2
  3from django.http import HttpRequest
  4from django_filters.filters import BooleanFilter
  5from django_filters.filterset import FilterSet
  6from drf_spectacular.utils import extend_schema
  7from guardian.shortcuts import get_anonymous_user
  8from rest_framework.decorators import action
  9from rest_framework.request import Request
 10from rest_framework.response import Response
 11from rest_framework.serializers import (
 12    CharField,
 13    ListField,
 14    PrimaryKeyRelatedField,
 15    Serializer,
 16)
 17from rest_framework.viewsets import ModelViewSet
 18from structlog.stdlib import get_logger
 19
 20from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
 21from authentik.core.api.groups import PartialUserSerializer
 22from authentik.core.api.used_by import UsedByMixin
 23from authentik.core.api.utils import JSONDictField, ModelSerializer
 24from authentik.core.models import User
 25from authentik.flows.api.flows import FlowSerializer
 26from authentik.flows.api.stages import StageSerializer
 27from authentik.lib.expression.evaluator import BaseEvaluator
 28from authentik.stages.invitation.models import Invitation, InvitationStage
 29
 30LOGGER = get_logger()
 31
 32
 33class InvitationStageSerializer(StageSerializer):
 34    """InvitationStage Serializer"""
 35
 36    class Meta:
 37        model = InvitationStage
 38        fields = StageSerializer.Meta.fields + [
 39            "continue_flow_without_invitation",
 40        ]
 41
 42
 43class InvitationStageFilter(FilterSet):
 44    """invitation filter"""
 45
 46    no_flows = BooleanFilter("flow", "isnull")
 47
 48    class Meta:
 49        model = InvitationStage
 50        fields = ["name", "no_flows", "continue_flow_without_invitation", "stage_uuid"]
 51
 52
 53class InvitationStageViewSet(UsedByMixin, ModelViewSet):
 54    """InvitationStage Viewset"""
 55
 56    queryset = InvitationStage.objects.all()
 57    serializer_class = InvitationStageSerializer
 58    filterset_class = InvitationStageFilter
 59    ordering = ["name"]
 60    search_fields = ["name"]
 61
 62
 63class InvitationSerializer(ModelSerializer):
 64    """Invitation Serializer"""
 65
 66    created_by = PartialUserSerializer(read_only=True)
 67    fixed_data = JSONDictField(required=False)
 68    flow_obj = FlowSerializer(read_only=True, required=False, source="flow")
 69
 70    def __init__(self, *args, **kwargs):
 71        super().__init__(*args, **kwargs)
 72        if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
 73            self.fields["created_by"] = PrimaryKeyRelatedField(
 74                queryset=User.objects.all(),
 75                required=False,
 76                allow_null=True,
 77                default=get_anonymous_user(),
 78            )
 79
 80    class Meta:
 81        model = Invitation
 82        fields = [
 83            "pk",
 84            "name",
 85            "expires",
 86            "fixed_data",
 87            "created_by",
 88            "single_use",
 89            "flow",
 90            "flow_obj",
 91        ]
 92
 93
 94class InvitationSendEmailSerializer(Serializer):
 95    """Serializer for sending invitation emails"""
 96
 97    email_addresses = ListField(required=True)
 98    cc_addresses = ListField(required=False)
 99    bcc_addresses = ListField(required=False)
100    template = CharField(required=False, default="invitation")
101
102
103class InvitationViewSet(UsedByMixin, ModelViewSet):
104    """Invitation Viewset"""
105
106    queryset = Invitation.objects.including_expired().all()
107    serializer_class = InvitationSerializer
108    ordering = ["-expires"]
109    search_fields = ["name", "created_by__username", "expires", "flow__slug"]
110    filterset_fields = ["name", "created_by__username", "expires", "flow__slug"]
111
112    def perform_create(self, serializer: InvitationSerializer):
113        kwargs = {}
114        if SERIALIZER_CONTEXT_BLUEPRINT not in serializer.context:
115            kwargs["created_by"] = self.request.user
116        serializer.save(**kwargs)
117
118    @extend_schema(
119        request=InvitationSendEmailSerializer,
120        responses={204: None},
121    )
122    @action(
123        detail=True,
124        methods=["post"],
125        serializer_class=InvitationSendEmailSerializer,
126    )
127    def send_email(self, request: Request, pk: str) -> Response:
128        """Send invitation link via email to one or more addresses"""
129        invitation = self.get_object()
130        email_addresses = request.data.get("email_addresses", [])
131        cc_addresses = request.data.get("cc_addresses", [])
132        bcc_addresses = request.data.get("bcc_addresses", [])
133        template = request.data.get("template", "email/invitation.html")
134
135        if not email_addresses:
136            return Response({"error": "No email addresses provided"}, status=400)
137
138        # Build the invitation link
139        http_request: HttpRequest = request._request
140        protocol = "https" if http_request.is_secure() else "http"
141        host = http_request.get_host()
142
143        # Determine the flow slug
144        flow_slug = invitation.flow.slug if invitation.flow else None
145        if not flow_slug:
146            return Response({"error": "Invitation has no associated flow"}, status=400)
147
148        invitation_link = f"{protocol}://{host}/if/flow/{flow_slug}/?itoken={invitation.pk}"
149
150        # Prepare template context
151        context = {
152            "url": invitation_link,
153            "expires": invitation.expires,
154            "host": host,
155        }
156
157        # Prepare email content
158        subject = f"You have been invited to {host}"
159
160        # Queue emails for sending via async ak_send_email
161        evaluator = BaseEvaluator()
162
163        for email in email_addresses:
164            evaluator.expr_send_email(
165                address=email,
166                subject=subject,
167                template=template,
168                context=context,
169                stage=None,
170                cc=cc_addresses if cc_addresses else None,
171                bcc=bcc_addresses if bcc_addresses else None,
172            )
173
174        return Response(status=204)
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
class InvitationStageSerializer(authentik.flows.api.stages.StageSerializer):
34class InvitationStageSerializer(StageSerializer):
35    """InvitationStage Serializer"""
36
37    class Meta:
38        model = InvitationStage
39        fields = StageSerializer.Meta.fields + [
40            "continue_flow_without_invitation",
41        ]

InvitationStage Serializer

class InvitationStageSerializer.Meta:
37    class Meta:
38        model = InvitationStage
39        fields = StageSerializer.Meta.fields + [
40            "continue_flow_without_invitation",
41        ]
fields = ['pk', 'name', 'component', 'verbose_name', 'verbose_name_plural', 'meta_model_name', 'flow_set', 'continue_flow_without_invitation']
class InvitationStageFilter(django_filters.filterset.FilterSet):
44class InvitationStageFilter(FilterSet):
45    """invitation filter"""
46
47    no_flows = BooleanFilter("flow", "isnull")
48
49    class Meta:
50        model = InvitationStage
51        fields = ["name", "no_flows", "continue_flow_without_invitation", "stage_uuid"]

invitation filter

no_flows
declared_filters = OrderedDict({'no_flows': <django_filters.filters.BooleanFilter object>})
base_filters = OrderedDict({'name': <django_filters.filters.CharFilter object>, 'no_flows': <django_filters.filters.BooleanFilter object>, 'continue_flow_without_invitation': <django_filters.filters.BooleanFilter object>, 'stage_uuid': <django_filters.filters.UUIDFilter object>})
class InvitationStageFilter.Meta:
49    class Meta:
50        model = InvitationStage
51        fields = ["name", "no_flows", "continue_flow_without_invitation", "stage_uuid"]
fields = ['name', 'no_flows', 'continue_flow_without_invitation', 'stage_uuid']
class InvitationStageViewSet(authentik.core.api.used_by.UsedByMixin, rest_framework.viewsets.ModelViewSet):
54class InvitationStageViewSet(UsedByMixin, ModelViewSet):
55    """InvitationStage Viewset"""
56
57    queryset = InvitationStage.objects.all()
58    serializer_class = InvitationStageSerializer
59    filterset_class = InvitationStageFilter
60    ordering = ["name"]
61    search_fields = ["name"]

InvitationStage Viewset

queryset = <InheritanceQuerySet []>
serializer_class = <class 'InvitationStageSerializer'>
filterset_class = <class 'InvitationStageFilter'>
ordering = ['name']
search_fields = ['name']
name = None
description = None
suffix = None
detail = None
basename = None
class InvitationSerializer(authentik.core.api.utils.ModelSerializer):
64class InvitationSerializer(ModelSerializer):
65    """Invitation Serializer"""
66
67    created_by = PartialUserSerializer(read_only=True)
68    fixed_data = JSONDictField(required=False)
69    flow_obj = FlowSerializer(read_only=True, required=False, source="flow")
70
71    def __init__(self, *args, **kwargs):
72        super().__init__(*args, **kwargs)
73        if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
74            self.fields["created_by"] = PrimaryKeyRelatedField(
75                queryset=User.objects.all(),
76                required=False,
77                allow_null=True,
78                default=get_anonymous_user(),
79            )
80
81    class Meta:
82        model = Invitation
83        fields = [
84            "pk",
85            "name",
86            "expires",
87            "fixed_data",
88            "created_by",
89            "single_use",
90            "flow",
91            "flow_obj",
92        ]

Invitation Serializer

InvitationSerializer(*args, **kwargs)
71    def __init__(self, *args, **kwargs):
72        super().__init__(*args, **kwargs)
73        if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
74            self.fields["created_by"] = PrimaryKeyRelatedField(
75                queryset=User.objects.all(),
76                required=False,
77                allow_null=True,
78                default=get_anonymous_user(),
79            )
created_by
fixed_data
flow_obj
class InvitationSerializer.Meta:
81    class Meta:
82        model = Invitation
83        fields = [
84            "pk",
85            "name",
86            "expires",
87            "fixed_data",
88            "created_by",
89            "single_use",
90            "flow",
91            "flow_obj",
92        ]
fields = ['pk', 'name', 'expires', 'fixed_data', 'created_by', 'single_use', 'flow', 'flow_obj']
class InvitationSendEmailSerializer(rest_framework.serializers.Serializer):
 95class InvitationSendEmailSerializer(Serializer):
 96    """Serializer for sending invitation emails"""
 97
 98    email_addresses = ListField(required=True)
 99    cc_addresses = ListField(required=False)
100    bcc_addresses = ListField(required=False)
101    template = CharField(required=False, default="invitation")

Serializer for sending invitation emails

email_addresses
cc_addresses
bcc_addresses
template
class InvitationViewSet(authentik.core.api.used_by.UsedByMixin, rest_framework.viewsets.ModelViewSet):
104class InvitationViewSet(UsedByMixin, ModelViewSet):
105    """Invitation Viewset"""
106
107    queryset = Invitation.objects.including_expired().all()
108    serializer_class = InvitationSerializer
109    ordering = ["-expires"]
110    search_fields = ["name", "created_by__username", "expires", "flow__slug"]
111    filterset_fields = ["name", "created_by__username", "expires", "flow__slug"]
112
113    def perform_create(self, serializer: InvitationSerializer):
114        kwargs = {}
115        if SERIALIZER_CONTEXT_BLUEPRINT not in serializer.context:
116            kwargs["created_by"] = self.request.user
117        serializer.save(**kwargs)
118
119    @extend_schema(
120        request=InvitationSendEmailSerializer,
121        responses={204: None},
122    )
123    @action(
124        detail=True,
125        methods=["post"],
126        serializer_class=InvitationSendEmailSerializer,
127    )
128    def send_email(self, request: Request, pk: str) -> Response:
129        """Send invitation link via email to one or more addresses"""
130        invitation = self.get_object()
131        email_addresses = request.data.get("email_addresses", [])
132        cc_addresses = request.data.get("cc_addresses", [])
133        bcc_addresses = request.data.get("bcc_addresses", [])
134        template = request.data.get("template", "email/invitation.html")
135
136        if not email_addresses:
137            return Response({"error": "No email addresses provided"}, status=400)
138
139        # Build the invitation link
140        http_request: HttpRequest = request._request
141        protocol = "https" if http_request.is_secure() else "http"
142        host = http_request.get_host()
143
144        # Determine the flow slug
145        flow_slug = invitation.flow.slug if invitation.flow else None
146        if not flow_slug:
147            return Response({"error": "Invitation has no associated flow"}, status=400)
148
149        invitation_link = f"{protocol}://{host}/if/flow/{flow_slug}/?itoken={invitation.pk}"
150
151        # Prepare template context
152        context = {
153            "url": invitation_link,
154            "expires": invitation.expires,
155            "host": host,
156        }
157
158        # Prepare email content
159        subject = f"You have been invited to {host}"
160
161        # Queue emails for sending via async ak_send_email
162        evaluator = BaseEvaluator()
163
164        for email in email_addresses:
165            evaluator.expr_send_email(
166                address=email,
167                subject=subject,
168                template=template,
169                context=context,
170                stage=None,
171                cc=cc_addresses if cc_addresses else None,
172                bcc=bcc_addresses if bcc_addresses else None,
173            )
174
175        return Response(status=204)

Invitation Viewset

queryset = <QuerySet []>
serializer_class = <class 'InvitationSerializer'>
ordering = ['-expires']
search_fields = ['name', 'created_by__username', 'expires', 'flow__slug']
filterset_fields = ['name', 'created_by__username', 'expires', 'flow__slug']
def perform_create( self, serializer: InvitationSerializer):
113    def perform_create(self, serializer: InvitationSerializer):
114        kwargs = {}
115        if SERIALIZER_CONTEXT_BLUEPRINT not in serializer.context:
116            kwargs["created_by"] = self.request.user
117        serializer.save(**kwargs)
@extend_schema(request=InvitationSendEmailSerializer, responses={204: None})
@action(detail=True, methods=['post'], serializer_class=InvitationSendEmailSerializer)
def send_email( self, request: rest_framework.request.Request, pk: str) -> rest_framework.response.Response:
119    @extend_schema(
120        request=InvitationSendEmailSerializer,
121        responses={204: None},
122    )
123    @action(
124        detail=True,
125        methods=["post"],
126        serializer_class=InvitationSendEmailSerializer,
127    )
128    def send_email(self, request: Request, pk: str) -> Response:
129        """Send invitation link via email to one or more addresses"""
130        invitation = self.get_object()
131        email_addresses = request.data.get("email_addresses", [])
132        cc_addresses = request.data.get("cc_addresses", [])
133        bcc_addresses = request.data.get("bcc_addresses", [])
134        template = request.data.get("template", "email/invitation.html")
135
136        if not email_addresses:
137            return Response({"error": "No email addresses provided"}, status=400)
138
139        # Build the invitation link
140        http_request: HttpRequest = request._request
141        protocol = "https" if http_request.is_secure() else "http"
142        host = http_request.get_host()
143
144        # Determine the flow slug
145        flow_slug = invitation.flow.slug if invitation.flow else None
146        if not flow_slug:
147            return Response({"error": "Invitation has no associated flow"}, status=400)
148
149        invitation_link = f"{protocol}://{host}/if/flow/{flow_slug}/?itoken={invitation.pk}"
150
151        # Prepare template context
152        context = {
153            "url": invitation_link,
154            "expires": invitation.expires,
155            "host": host,
156        }
157
158        # Prepare email content
159        subject = f"You have been invited to {host}"
160
161        # Queue emails for sending via async ak_send_email
162        evaluator = BaseEvaluator()
163
164        for email in email_addresses:
165            evaluator.expr_send_email(
166                address=email,
167                subject=subject,
168                template=template,
169                context=context,
170                stage=None,
171                cc=cc_addresses if cc_addresses else None,
172                bcc=bcc_addresses if bcc_addresses else None,
173            )
174
175        return Response(status=204)

Send invitation link via email to one or more addresses

name = None
description = None
suffix = None
detail = None
basename = None