authentik.enterprise.lifecycle.api.iterations

  1from datetime import datetime
  2
  3from django.db.models import Exists, OuterRef, Q, Subquery
  4from django_filters.rest_framework import BooleanFilter, FilterSet
  5from drf_spectacular.utils import extend_schema
  6from rest_framework.decorators import action
  7from rest_framework.fields import IntegerField, SerializerMethodField
  8from rest_framework.mixins import CreateModelMixin
  9from rest_framework.request import Request
 10from rest_framework.response import Response
 11from rest_framework.viewsets import GenericViewSet
 12
 13from authentik.core.api.utils import ModelSerializer
 14from authentik.enterprise.api import EnterpriseRequiredMixin
 15from authentik.enterprise.lifecycle.api.reviews import ReviewSerializer
 16from authentik.enterprise.lifecycle.models import LifecycleIteration, LifecycleRule, ReviewState
 17from authentik.enterprise.lifecycle.utils import (
 18    ContentTypeField,
 19    ReviewerGroupSerializer,
 20    ReviewerUserSerializer,
 21    admin_link_for_model,
 22    parse_content_type,
 23    start_of_day,
 24)
 25from authentik.lib.utils.time import timedelta_from_string
 26
 27
 28class RelatedRuleSerializer(EnterpriseRequiredMixin, ModelSerializer):
 29    reviewer_groups = ReviewerGroupSerializer(many=True, read_only=True)
 30    min_reviewers = IntegerField(read_only=True)
 31    reviewers = ReviewerUserSerializer(many=True, read_only=True)
 32
 33    class Meta:
 34        model = LifecycleRule
 35        fields = ["id", "name", "reviewer_groups", "min_reviewers", "reviewers"]
 36
 37
 38class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
 39    content_type = ContentTypeField()
 40    object_verbose = SerializerMethodField()
 41    rule = RelatedRuleSerializer(read_only=True)
 42    object_admin_url = SerializerMethodField(read_only=True)
 43    grace_period_end = SerializerMethodField(read_only=True)
 44    reviews = ReviewSerializer(many=True, read_only=True, source="review_set.all")
 45    user_can_review = SerializerMethodField(read_only=True)
 46
 47    next_review_date = SerializerMethodField(read_only=True)
 48
 49    class Meta:
 50        model = LifecycleIteration
 51        fields = [
 52            "id",
 53            "content_type",
 54            "object_id",
 55            "object_verbose",
 56            "object_admin_url",
 57            "state",
 58            "opened_on",
 59            "grace_period_end",
 60            "next_review_date",
 61            "reviews",
 62            "rule",
 63            "user_can_review",
 64        ]
 65        read_only_fields = fields
 66
 67    def get_object_verbose(self, iteration: LifecycleIteration) -> str:
 68        return str(iteration.object)
 69
 70    def get_object_admin_url(self, iteration: LifecycleIteration) -> str:
 71        return admin_link_for_model(iteration.object)
 72
 73    def get_grace_period_end(self, iteration: LifecycleIteration) -> datetime:
 74        return start_of_day(
 75            iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
 76        )
 77
 78    def get_next_review_date(self, iteration: LifecycleIteration) -> datetime:
 79        return start_of_day(iteration.opened_on + timedelta_from_string(iteration.rule.interval))
 80
 81    def get_user_can_review(self, iteration: LifecycleIteration) -> bool:
 82        return iteration.user_can_review(self.context["request"].user)
 83
 84
 85class LifecycleIterationFilterSet(FilterSet):
 86    user_is_reviewer = BooleanFilter(field_name="user_is_reviewer", lookup_expr="exact")
 87
 88
 89class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet):
 90    queryset = LifecycleIteration.objects.all()
 91    serializer_class = LifecycleIterationSerializer
 92    ordering = ["-opened_on"]
 93    ordering_fields = [
 94        "state",
 95        "content_type__model",
 96        "rule__name",
 97        "opened_on",
 98        "grace_period_end",
 99    ]
100    filterset_class = LifecycleIterationFilterSet
101
102    def get_queryset(self):
103        user = self.request.user
104        return self.queryset.annotate(
105            user_is_reviewer=Exists(
106                LifecycleRule.objects.filter(
107                    pk=OuterRef("rule_id"),
108                ).filter(
109                    Q(reviewers=user) | Q(reviewer_groups__in=user.groups.all().with_ancestors())
110                )
111            )
112        )
113
114    @extend_schema(
115        operation_id="lifecycle_iterations_list_latest",
116        responses={200: LifecycleIterationSerializer(many=True)},
117    )
118    @action(
119        detail=False,
120        pagination_class=None,
121        methods=["get"],
122        url_path=r"latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)",
123    )
124    def latest_iterations(self, request: Request, content_type: str, object_id: str) -> Response:
125        ct = parse_content_type(content_type)
126        latest_ids_subquery = (
127            LifecycleIteration.objects.filter(
128                rule=OuterRef("rule"),
129                content_type__app_label=ct["app_label"],
130                content_type__model=ct["model"],
131                object_id=object_id,
132            )
133            .order_by("-opened_on")
134            .values("id")[:1]
135        )
136        latest_per_rule = LifecycleIteration.objects.filter(
137            content_type__app_label=ct["app_label"],
138            content_type__model=ct["model"],
139            object_id=object_id,
140        ).filter(id=Subquery(latest_ids_subquery))
141        serializer = self.get_serializer(latest_per_rule, many=True)
142        return Response(serializer.data)
143
144    @extend_schema(
145        operation_id="lifecycle_iterations_list_open",
146        responses={200: LifecycleIterationSerializer(many=True)},
147    )
148    @action(
149        detail=False,
150        methods=["get"],
151        url_path=r"open",
152    )
153    def open_iterations(self, request: Request):
154        iterations = self.get_queryset().filter(
155            Q(state=ReviewState.PENDING) | Q(state=ReviewState.OVERDUE)
156        )
157        iterations = self.filter_queryset(iterations)
158        page = self.paginate_queryset(iterations)
159        if page is not None:
160            serializer = self.get_serializer(page, many=True)
161            return self.get_paginated_response(serializer.data)
162
163        serializer = self.get_serializer(iterations, many=True)
164        return Response(serializer.data)
29class RelatedRuleSerializer(EnterpriseRequiredMixin, ModelSerializer):
30    reviewer_groups = ReviewerGroupSerializer(many=True, read_only=True)
31    min_reviewers = IntegerField(read_only=True)
32    reviewers = ReviewerUserSerializer(many=True, read_only=True)
33
34    class Meta:
35        model = LifecycleRule
36        fields = ["id", "name", "reviewer_groups", "min_reviewers", "reviewers"]

Mixin to validate that a valid enterprise license exists before allowing to save the object

reviewer_groups
min_reviewers
reviewers
class RelatedRuleSerializer.Meta:
34    class Meta:
35        model = LifecycleRule
36        fields = ["id", "name", "reviewer_groups", "min_reviewers", "reviewers"]
fields = ['id', 'name', 'reviewer_groups', 'min_reviewers', 'reviewers']
39class LifecycleIterationSerializer(EnterpriseRequiredMixin, ModelSerializer):
40    content_type = ContentTypeField()
41    object_verbose = SerializerMethodField()
42    rule = RelatedRuleSerializer(read_only=True)
43    object_admin_url = SerializerMethodField(read_only=True)
44    grace_period_end = SerializerMethodField(read_only=True)
45    reviews = ReviewSerializer(many=True, read_only=True, source="review_set.all")
46    user_can_review = SerializerMethodField(read_only=True)
47
48    next_review_date = SerializerMethodField(read_only=True)
49
50    class Meta:
51        model = LifecycleIteration
52        fields = [
53            "id",
54            "content_type",
55            "object_id",
56            "object_verbose",
57            "object_admin_url",
58            "state",
59            "opened_on",
60            "grace_period_end",
61            "next_review_date",
62            "reviews",
63            "rule",
64            "user_can_review",
65        ]
66        read_only_fields = fields
67
68    def get_object_verbose(self, iteration: LifecycleIteration) -> str:
69        return str(iteration.object)
70
71    def get_object_admin_url(self, iteration: LifecycleIteration) -> str:
72        return admin_link_for_model(iteration.object)
73
74    def get_grace_period_end(self, iteration: LifecycleIteration) -> datetime:
75        return start_of_day(
76            iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
77        )
78
79    def get_next_review_date(self, iteration: LifecycleIteration) -> datetime:
80        return start_of_day(iteration.opened_on + timedelta_from_string(iteration.rule.interval))
81
82    def get_user_can_review(self, iteration: LifecycleIteration) -> bool:
83        return iteration.user_can_review(self.context["request"].user)

Mixin to validate that a valid enterprise license exists before allowing to save the object

content_type
object_verbose
rule
object_admin_url
grace_period_end
reviews
user_can_review
next_review_date
def get_object_verbose( self, iteration: authentik.enterprise.lifecycle.models.LifecycleIteration) -> str:
68    def get_object_verbose(self, iteration: LifecycleIteration) -> str:
69        return str(iteration.object)
def get_object_admin_url( self, iteration: authentik.enterprise.lifecycle.models.LifecycleIteration) -> str:
71    def get_object_admin_url(self, iteration: LifecycleIteration) -> str:
72        return admin_link_for_model(iteration.object)
def get_grace_period_end( self, iteration: authentik.enterprise.lifecycle.models.LifecycleIteration) -> datetime.datetime:
74    def get_grace_period_end(self, iteration: LifecycleIteration) -> datetime:
75        return start_of_day(
76            iteration.opened_on + timedelta_from_string(iteration.rule.grace_period)
77        )
def get_next_review_date( self, iteration: authentik.enterprise.lifecycle.models.LifecycleIteration) -> datetime.datetime:
79    def get_next_review_date(self, iteration: LifecycleIteration) -> datetime:
80        return start_of_day(iteration.opened_on + timedelta_from_string(iteration.rule.interval))
def get_user_can_review( self, iteration: authentik.enterprise.lifecycle.models.LifecycleIteration) -> bool:
82    def get_user_can_review(self, iteration: LifecycleIteration) -> bool:
83        return iteration.user_can_review(self.context["request"].user)
class LifecycleIterationSerializer.Meta:
50    class Meta:
51        model = LifecycleIteration
52        fields = [
53            "id",
54            "content_type",
55            "object_id",
56            "object_verbose",
57            "object_admin_url",
58            "state",
59            "opened_on",
60            "grace_period_end",
61            "next_review_date",
62            "reviews",
63            "rule",
64            "user_can_review",
65        ]
66        read_only_fields = fields
fields = ['id', 'content_type', 'object_id', 'object_verbose', 'object_admin_url', 'state', 'opened_on', 'grace_period_end', 'next_review_date', 'reviews', 'rule', 'user_can_review']
read_only_fields = ['id', 'content_type', 'object_id', 'object_verbose', 'object_admin_url', 'state', 'opened_on', 'grace_period_end', 'next_review_date', 'reviews', 'rule', 'user_can_review']
class LifecycleIterationFilterSet(django_filters.rest_framework.filterset.FilterSet):
86class LifecycleIterationFilterSet(FilterSet):
87    user_is_reviewer = BooleanFilter(field_name="user_is_reviewer", lookup_expr="exact")
user_is_reviewer
declared_filters = OrderedDict({'user_is_reviewer': <django_filters.rest_framework.filters.BooleanFilter object>})
base_filters = OrderedDict({'user_is_reviewer': <django_filters.rest_framework.filters.BooleanFilter object>})
class IterationViewSet(authentik.enterprise.api.EnterpriseRequiredMixin, rest_framework.mixins.CreateModelMixin, rest_framework.viewsets.GenericViewSet):
 90class IterationViewSet(EnterpriseRequiredMixin, CreateModelMixin, GenericViewSet):
 91    queryset = LifecycleIteration.objects.all()
 92    serializer_class = LifecycleIterationSerializer
 93    ordering = ["-opened_on"]
 94    ordering_fields = [
 95        "state",
 96        "content_type__model",
 97        "rule__name",
 98        "opened_on",
 99        "grace_period_end",
100    ]
101    filterset_class = LifecycleIterationFilterSet
102
103    def get_queryset(self):
104        user = self.request.user
105        return self.queryset.annotate(
106            user_is_reviewer=Exists(
107                LifecycleRule.objects.filter(
108                    pk=OuterRef("rule_id"),
109                ).filter(
110                    Q(reviewers=user) | Q(reviewer_groups__in=user.groups.all().with_ancestors())
111                )
112            )
113        )
114
115    @extend_schema(
116        operation_id="lifecycle_iterations_list_latest",
117        responses={200: LifecycleIterationSerializer(many=True)},
118    )
119    @action(
120        detail=False,
121        pagination_class=None,
122        methods=["get"],
123        url_path=r"latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)",
124    )
125    def latest_iterations(self, request: Request, content_type: str, object_id: str) -> Response:
126        ct = parse_content_type(content_type)
127        latest_ids_subquery = (
128            LifecycleIteration.objects.filter(
129                rule=OuterRef("rule"),
130                content_type__app_label=ct["app_label"],
131                content_type__model=ct["model"],
132                object_id=object_id,
133            )
134            .order_by("-opened_on")
135            .values("id")[:1]
136        )
137        latest_per_rule = LifecycleIteration.objects.filter(
138            content_type__app_label=ct["app_label"],
139            content_type__model=ct["model"],
140            object_id=object_id,
141        ).filter(id=Subquery(latest_ids_subquery))
142        serializer = self.get_serializer(latest_per_rule, many=True)
143        return Response(serializer.data)
144
145    @extend_schema(
146        operation_id="lifecycle_iterations_list_open",
147        responses={200: LifecycleIterationSerializer(many=True)},
148    )
149    @action(
150        detail=False,
151        methods=["get"],
152        url_path=r"open",
153    )
154    def open_iterations(self, request: Request):
155        iterations = self.get_queryset().filter(
156            Q(state=ReviewState.PENDING) | Q(state=ReviewState.OVERDUE)
157        )
158        iterations = self.filter_queryset(iterations)
159        page = self.paginate_queryset(iterations)
160        if page is not None:
161            serializer = self.get_serializer(page, many=True)
162            return self.get_paginated_response(serializer.data)
163
164        serializer = self.get_serializer(iterations, many=True)
165        return Response(serializer.data)

Mixin to validate that a valid enterprise license exists before allowing to save the object

queryset = <QuerySet []>
serializer_class = <class 'LifecycleIterationSerializer'>
ordering = ['-opened_on']
ordering_fields = ['state', 'content_type__model', 'rule__name', 'opened_on', 'grace_period_end']
filterset_class = <class 'LifecycleIterationFilterSet'>
def get_queryset(self):
103    def get_queryset(self):
104        user = self.request.user
105        return self.queryset.annotate(
106            user_is_reviewer=Exists(
107                LifecycleRule.objects.filter(
108                    pk=OuterRef("rule_id"),
109                ).filter(
110                    Q(reviewers=user) | Q(reviewer_groups__in=user.groups.all().with_ancestors())
111                )
112            )
113        )

Get the list of items for this view. This must be an iterable, and may be a queryset. Defaults to using self.queryset.

This method should always be used rather than accessing self.queryset directly, as self.queryset gets evaluated only once, and those results are cached for all subsequent requests.

You may want to override this if you need to provide different querysets depending on the incoming request.

(Eg. return a list of items that is specific to the user)

@extend_schema(operation_id='lifecycle_iterations_list_latest', responses={200: LifecycleIterationSerializer(many=True)})
@action(detail=False, pagination_class=None, methods=['get'], url_path='latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)')
def latest_iterations( self, request: rest_framework.request.Request, content_type: str, object_id: str) -> rest_framework.response.Response:
115    @extend_schema(
116        operation_id="lifecycle_iterations_list_latest",
117        responses={200: LifecycleIterationSerializer(many=True)},
118    )
119    @action(
120        detail=False,
121        pagination_class=None,
122        methods=["get"],
123        url_path=r"latest/(?P<content_type>[^/]+)/(?P<object_id>[^/]+)",
124    )
125    def latest_iterations(self, request: Request, content_type: str, object_id: str) -> Response:
126        ct = parse_content_type(content_type)
127        latest_ids_subquery = (
128            LifecycleIteration.objects.filter(
129                rule=OuterRef("rule"),
130                content_type__app_label=ct["app_label"],
131                content_type__model=ct["model"],
132                object_id=object_id,
133            )
134            .order_by("-opened_on")
135            .values("id")[:1]
136        )
137        latest_per_rule = LifecycleIteration.objects.filter(
138            content_type__app_label=ct["app_label"],
139            content_type__model=ct["model"],
140            object_id=object_id,
141        ).filter(id=Subquery(latest_ids_subquery))
142        serializer = self.get_serializer(latest_per_rule, many=True)
143        return Response(serializer.data)
@extend_schema(operation_id='lifecycle_iterations_list_open', responses={200: LifecycleIterationSerializer(many=True)})
@action(detail=False, methods=['get'], url_path='open')
def open_iterations(self, request: rest_framework.request.Request):
145    @extend_schema(
146        operation_id="lifecycle_iterations_list_open",
147        responses={200: LifecycleIterationSerializer(many=True)},
148    )
149    @action(
150        detail=False,
151        methods=["get"],
152        url_path=r"open",
153    )
154    def open_iterations(self, request: Request):
155        iterations = self.get_queryset().filter(
156            Q(state=ReviewState.PENDING) | Q(state=ReviewState.OVERDUE)
157        )
158        iterations = self.filter_queryset(iterations)
159        page = self.paginate_queryset(iterations)
160        if page is not None:
161            serializer = self.get_serializer(page, many=True)
162            return self.get_paginated_response(serializer.data)
163
164        serializer = self.get_serializer(iterations, many=True)
165        return Response(serializer.data)
name = None
description = None
suffix = None
detail = None
basename = None