Related to https://github.com/grafana/oncall-private/issues/2826 RBAC enabled or not (OSS or cloud), it is possible to get service account permissions, enabling perm check (for service account tokens) in public API. Also allow empty value for users in sync (instead of returning a 400 response).
304 lines
13 KiB
Python
304 lines
13 KiB
Python
from django.db.models import Q
|
|
from django_filters import rest_framework as filters
|
|
from rest_framework import mixins, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.exceptions import NotFound
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.viewsets import GenericViewSet
|
|
|
|
from apps.alerts.constants import ActionSource
|
|
from apps.alerts.models import AlertGroup, AlertReceiveChannel
|
|
from apps.alerts.tasks import delete_alert_group, wipe
|
|
from apps.api.label_filtering import parse_label_query
|
|
from apps.api.permissions import RBACPermission
|
|
from apps.auth_token.auth import ApiTokenAuthentication, GrafanaServiceAccountAuthentication
|
|
from apps.public_api.constants import VALID_DATE_FOR_DELETE_INCIDENT
|
|
from apps.public_api.helpers import is_valid_group_creation_date, team_has_slack_token_for_deleting
|
|
from apps.public_api.serializers import AlertGroupSerializer
|
|
from apps.public_api.throttlers.user_throttle import UserThrottle
|
|
from common.api_helpers.exceptions import BadRequest, Forbidden
|
|
from common.api_helpers.filters import (
|
|
NO_TEAM_VALUE,
|
|
ByTeamModelFieldFilterMixin,
|
|
DateRangeFilterMixin,
|
|
get_team_queryset,
|
|
)
|
|
from common.api_helpers.mixins import AlertGroupEnrichingMixin, RateLimitHeadersMixin
|
|
from common.api_helpers.paginators import FiftyPageSizePaginator
|
|
|
|
|
|
class AlertGroupFilters(ByTeamModelFieldFilterMixin, DateRangeFilterMixin, filters.FilterSet):
|
|
# query field param name to filter by team
|
|
TEAM_FILTER_FIELD_NAME = "team_id"
|
|
|
|
id = filters.CharFilter(field_name="public_primary_key")
|
|
|
|
team_id = filters.ModelChoiceFilter(
|
|
field_name="channel__team",
|
|
queryset=get_team_queryset,
|
|
to_field_name="public_primary_key",
|
|
null_label="noteam",
|
|
null_value=NO_TEAM_VALUE,
|
|
method=ByTeamModelFieldFilterMixin.filter_model_field_with_single_value.__name__,
|
|
)
|
|
|
|
started_at = filters.CharFilter(
|
|
field_name="started_at",
|
|
method=DateRangeFilterMixin.filter_date_range.__name__,
|
|
)
|
|
|
|
|
|
class AlertGroupView(
|
|
AlertGroupEnrichingMixin,
|
|
RateLimitHeadersMixin,
|
|
mixins.ListModelMixin,
|
|
mixins.RetrieveModelMixin,
|
|
mixins.DestroyModelMixin,
|
|
GenericViewSet,
|
|
):
|
|
authentication_classes = (GrafanaServiceAccountAuthentication, ApiTokenAuthentication)
|
|
permission_classes = (IsAuthenticated, RBACPermission)
|
|
|
|
rbac_permissions = {
|
|
"list": [RBACPermission.Permissions.ALERT_GROUPS_READ],
|
|
"retrieve": [RBACPermission.Permissions.ALERT_GROUPS_READ],
|
|
"destroy": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
"acknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
"unacknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
"resolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
"unresolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
"silence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
"unsilence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE],
|
|
}
|
|
|
|
throttle_classes = [UserThrottle]
|
|
|
|
model = AlertGroup
|
|
serializer_class = AlertGroupSerializer
|
|
pagination_class = FiftyPageSizePaginator
|
|
|
|
filter_backends = (filters.DjangoFilterBackend,)
|
|
filterset_class = AlertGroupFilters
|
|
|
|
def get_queryset(self):
|
|
# no select_related or prefetch_related is used at this point, it will be done on paginate_queryset.
|
|
|
|
route_id = self.request.query_params.get("route_id", None)
|
|
integration_id = self.request.query_params.get("integration_id", None)
|
|
state = self.request.query_params.get("state", None)
|
|
|
|
alert_receive_channels_qs = AlertReceiveChannel.objects_with_deleted.filter(
|
|
organization_id=self.request.auth.organization.id
|
|
)
|
|
if integration_id:
|
|
alert_receive_channels_qs = alert_receive_channels_qs.filter(public_primary_key=integration_id)
|
|
|
|
alert_receive_channels_ids = list(alert_receive_channels_qs.values_list("id", flat=True))
|
|
queryset = AlertGroup.objects.filter(channel__in=alert_receive_channels_ids).order_by("-started_at")
|
|
|
|
if route_id:
|
|
queryset = queryset.filter(channel_filter__public_primary_key=route_id)
|
|
if state:
|
|
choices = dict(AlertGroup.STATUS_CHOICES)
|
|
try:
|
|
choice = [i for i in choices if choices[i] == state.lower().capitalize()][0]
|
|
status_filter = Q()
|
|
if choice == AlertGroup.NEW:
|
|
status_filter = AlertGroup.get_new_state_filter()
|
|
elif choice == AlertGroup.SILENCED:
|
|
status_filter = AlertGroup.get_silenced_state_filter()
|
|
elif choice == AlertGroup.ACKNOWLEDGED:
|
|
status_filter = AlertGroup.get_acknowledged_state_filter()
|
|
elif choice == AlertGroup.RESOLVED:
|
|
status_filter = AlertGroup.get_resolved_state_filter()
|
|
queryset = queryset.filter(status_filter)
|
|
except IndexError:
|
|
valid_choices_text = ", ".join(
|
|
[status_choice[1].lower() for status_choice in AlertGroup.STATUS_CHOICES]
|
|
)
|
|
raise BadRequest(detail={"state": f"Must be one of the following: {valid_choices_text}"})
|
|
|
|
# filter by alert group (static, applied) labels
|
|
label_query = self.request.query_params.getlist("label", [])
|
|
kv_pairs = parse_label_query(label_query)
|
|
for key, value in kv_pairs:
|
|
queryset = queryset.filter(
|
|
labels__organization=self.request.auth.organization,
|
|
labels__key_name=key,
|
|
labels__value_name=value,
|
|
)
|
|
|
|
return queryset
|
|
|
|
def get_object(self):
|
|
public_primary_key = self.kwargs["pk"]
|
|
|
|
try:
|
|
obj = AlertGroup.objects.filter(
|
|
channel__organization=self.request.auth.organization,
|
|
).get(public_primary_key=public_primary_key)
|
|
obj = self.enrich([obj])[0]
|
|
return obj
|
|
except AlertGroup.DoesNotExist:
|
|
raise NotFound
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
instance = self.get_object()
|
|
if not isinstance(request.data, dict):
|
|
return Response(data="A dict with a `mode` key is expected", status=status.HTTP_400_BAD_REQUEST)
|
|
mode = request.data.get("mode", "wipe")
|
|
if mode == "delete":
|
|
if not team_has_slack_token_for_deleting(instance):
|
|
raise BadRequest(
|
|
detail="Your OnCall Bot in Slack is outdated. Please reinstall OnCall Bot and try again."
|
|
)
|
|
elif not is_valid_group_creation_date(instance):
|
|
raise BadRequest(
|
|
detail=f"We are unable to “delete” old alert_groups (created before "
|
|
f"{VALID_DATE_FOR_DELETE_INCIDENT.strftime('%d %B %Y')}) using API. "
|
|
f"Please use “wipe” mode or contact help. Sorry for that!"
|
|
)
|
|
else:
|
|
delete_alert_group.apply_async((instance.pk, request.user.pk))
|
|
elif mode == "wipe":
|
|
wipe.apply_async((instance.pk, request.user.pk))
|
|
else:
|
|
return Response(data="Invalid mode", status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def acknowledge(self, request, pk):
|
|
if request.user.is_service_account:
|
|
raise Forbidden(detail="Service accounts are not allowed to acknowledge alert groups")
|
|
|
|
alert_group = self.get_object()
|
|
|
|
if alert_group.acknowledged:
|
|
raise BadRequest(detail="Can't acknowledge an acknowledged alert group")
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't acknowledge a resolved alert group")
|
|
|
|
if alert_group.root_alert_group:
|
|
raise BadRequest(detail="Can't acknowledge an attached alert group")
|
|
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't acknowledge a maintenance alert group")
|
|
|
|
alert_group.acknowledge_by_user_or_backsync(self.request.user, action_source=ActionSource.API)
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unacknowledge(self, request, pk):
|
|
if request.user.is_service_account:
|
|
raise Forbidden(detail="Service accounts are not allowed to unacknowledge alert groups")
|
|
|
|
alert_group = self.get_object()
|
|
|
|
if not alert_group.acknowledged:
|
|
raise BadRequest(detail="Can't unacknowledge an unacknowledged alert group")
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't unacknowledge a resolved alert group")
|
|
|
|
if alert_group.root_alert_group:
|
|
raise BadRequest(detail="Can't unacknowledge an attached alert group")
|
|
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't unacknowledge a maintenance alert group")
|
|
|
|
alert_group.un_acknowledge_by_user_or_backsync(self.request.user, action_source=ActionSource.API)
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def resolve(self, request, pk):
|
|
if request.user.is_service_account:
|
|
raise Forbidden(detail="Service accounts are not allowed to resolve alert groups")
|
|
|
|
alert_group = self.get_object()
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't resolve a resolved alert group")
|
|
|
|
if alert_group.root_alert_group:
|
|
raise BadRequest(detail="Can't resolve an attached alert group")
|
|
|
|
if alert_group.is_maintenance_incident:
|
|
alert_group.stop_maintenance(self.request.user)
|
|
else:
|
|
alert_group.resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.API)
|
|
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unresolve(self, request, pk):
|
|
if request.user.is_service_account:
|
|
raise Forbidden(detail="Service accounts are not allowed to unresolve alert groups")
|
|
|
|
alert_group = self.get_object()
|
|
|
|
if not alert_group.resolved:
|
|
raise BadRequest(detail="Can't unresolve an unresolved alert group")
|
|
|
|
if alert_group.root_alert_group:
|
|
raise BadRequest(detail="Can't unresolve an attached alert group")
|
|
|
|
if alert_group.is_maintenance_incident:
|
|
raise BadRequest(detail="Can't unresolve a maintenance alert group")
|
|
|
|
alert_group.un_resolve_by_user_or_backsync(self.request.user, action_source=ActionSource.API)
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def silence(self, request, pk=None):
|
|
if request.user.is_service_account:
|
|
raise Forbidden(detail="Service accounts are not allowed to silence alert groups")
|
|
|
|
alert_group = self.get_object()
|
|
|
|
delay = request.data.get("delay")
|
|
if delay is None:
|
|
raise BadRequest(detail="delay is required")
|
|
try:
|
|
delay = int(delay)
|
|
except ValueError:
|
|
raise BadRequest(detail="invalid delay value")
|
|
if delay < -1:
|
|
raise BadRequest(detail="invalid delay value")
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't silence a resolved alert group")
|
|
|
|
if alert_group.acknowledged:
|
|
raise BadRequest(detail="Can't silence an acknowledged alert group")
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't silence an attached alert group")
|
|
|
|
alert_group.silence_by_user_or_backsync(request.user, silence_delay=delay, action_source=ActionSource.API)
|
|
return Response(status=status.HTTP_200_OK)
|
|
|
|
@action(methods=["post"], detail=True)
|
|
def unsilence(self, request, pk=None):
|
|
if request.user.is_service_account:
|
|
raise Forbidden(detail="Service accounts are not allowed to unsilence alert groups")
|
|
|
|
alert_group = self.get_object()
|
|
|
|
if not alert_group.silenced:
|
|
raise BadRequest(detail="Can't unsilence an unsilenced alert group")
|
|
|
|
if alert_group.resolved:
|
|
raise BadRequest(detail="Can't unsilence a resolved alert group")
|
|
|
|
if alert_group.acknowledged:
|
|
raise BadRequest(detail="Can't unsilence an acknowledged alert group")
|
|
|
|
if alert_group.root_alert_group is not None:
|
|
raise BadRequest(detail="Can't unsilence an attached alert group")
|
|
|
|
alert_group.un_silence_by_user_or_backsync(request.user, action_source=ActionSource.API)
|
|
|
|
return Response(status=status.HTTP_200_OK)
|