from collections import defaultdict from rest_framework import fields, serializers from rest_framework.validators import UniqueTogetherValidator from apps.user_management.models import ServiceAccountUser from apps.webhooks.models import Webhook, WebhookResponse from apps.webhooks.models.webhook import PUBLIC_WEBHOOK_HTTP_METHODS, WEBHOOK_FIELD_PLACEHOLDER from apps.webhooks.presets.preset_options import WebhookPresetOptions from common.api_helpers.custom_fields import IntegrationFilteredByOrganizationField, TeamPrimaryKeyRelatedField from common.api_helpers.exceptions import BadRequest from common.api_helpers.mixins import EagerLoadingMixin from common.api_helpers.utils import CurrentOrganizationDefault, CurrentTeamDefault, CurrentUserDefault from common.jinja_templater import apply_jinja_template from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning PRESET_VALIDATION_MESSAGE = "Preset webhooks must be modified through web UI" INTEGRATION_FILTER_MESSAGE = "integration_filter must be a list of valid integration ids" class WebhookTriggerTypeField(fields.CharField): def to_representation(self, value): return Webhook.PUBLIC_TRIGGER_TYPES_MAP[value] def to_internal_value(self, data): try: trigger_type = [ key for key, value in Webhook.PUBLIC_TRIGGER_TYPES_MAP.items() if value == data and key in Webhook.PUBLIC_TRIGGER_TYPES_MAP ][0] except IndexError: raise BadRequest(detail=f"trigger_type must one of {Webhook.PUBLIC_ALL_TRIGGER_TYPES}") return trigger_type class WebhookResponseSerializer(serializers.ModelSerializer): class Meta: model = WebhookResponse fields = [ "timestamp", "url", "request_trigger", "request_headers", "request_data", "status_code", "content", "event_data", ] class WebhookCreateSerializer(EagerLoadingMixin, serializers.ModelSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") organization = serializers.HiddenField(default=CurrentOrganizationDefault()) team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault()) user = serializers.HiddenField(default=CurrentUserDefault()) trigger_type = WebhookTriggerTypeField() integration_filter = IntegrationFilteredByOrganizationField( source="filtered_integrations", many=True, required=False ) SELECT_RELATED = ["organization", "team"] class Meta: model = Webhook fields = [ "id", "name", "is_webhook_enabled", "organization", "team", "user", "data", "username", "password", "authorization_header", "trigger_template", "headers", "url", "forward_all", "http_method", "trigger_type", "integration_filter", "preset", ] extra_kwargs = { "name": {"required": True, "allow_null": False, "allow_blank": False}, "url": {"required": True, "allow_null": False, "allow_blank": False}, "http_method": {"required": True, "allow_null": False, "allow_blank": False}, "username": {"required": False, "allow_null": True, "allow_blank": True}, "password": {"required": False, "allow_null": True, "allow_blank": True}, "authorization_header": {"required": False, "allow_null": True, "allow_blank": True}, "trigger_template": {"required": False, "allow_null": True, "allow_blank": True}, "headers": {"required": False, "allow_null": True, "allow_blank": True}, "data": {"required": False, "allow_null": True, "allow_blank": True}, "forward_all": {"required": False, "allow_null": False}, } validators = [UniqueTogetherValidator(queryset=Webhook.objects.all(), fields=["name", "organization"])] def to_representation(self, instance): result = super().to_representation(instance) if instance.password: result["password"] = WEBHOOK_FIELD_PLACEHOLDER if instance.authorization_header: result["authorization_header"] = WEBHOOK_FIELD_PLACEHOLDER if instance.filtered_integrations.count() == 0: result["integration_filter"] = None return result def to_internal_value(self, data): webhook = self.instance if data.get("password") == WEBHOOK_FIELD_PLACEHOLDER: data["password"] = webhook.password if data.get("authorization_header") == WEBHOOK_FIELD_PLACEHOLDER: data["authorization_header"] = webhook.authorization_header if not data.get("integration_filter"): data["integration_filter"] = [] return super().to_internal_value(data) def _validate_template_field(self, template): try: apply_jinja_template(template, alert_payload=defaultdict(str), alert_group_id="alert_group_1") except JinjaTemplateError as e: raise serializers.ValidationError(e.fallback_message) except JinjaTemplateWarning: # Suppress render exceptions since we do not have a representative payload to test with pass return template def validate_trigger_template(self, trigger_template): if not trigger_template: return None return self._validate_template_field(trigger_template) def validate_headers(self, headers): if not headers: return None return self._validate_template_field(headers) def validate_url(self, url): if not url: return None return self._validate_template_field(url) def validate_data(self, data): if not data: return None return self._validate_template_field(data) def validate_forward_all(self, data): if data is None: return False return data def validate_http_method(self, http_method): if http_method not in PUBLIC_WEBHOOK_HTTP_METHODS: raise serializers.ValidationError(f"Must be one of {PUBLIC_WEBHOOK_HTTP_METHODS}") return http_method def validate_preset(self, preset): raise serializers.ValidationError(PRESET_VALIDATION_MESSAGE) def validate_user(self, user): # user may also be a string when handling requests from the deprecated custom action API if isinstance(user, ServiceAccountUser): return None return user def validate(self, data): if ( self.instance and self.instance.preset and WebhookPresetOptions.ADVANCED_PRESET_META_DATA and WebhookPresetOptions.ADVANCED_PRESET_META_DATA.id and self.instance.preset != WebhookPresetOptions.ADVANCED_PRESET_META_DATA.id ): raise serializers.ValidationError(PRESET_VALIDATION_MESSAGE) return data class WebhookUpdateSerializer(WebhookCreateSerializer): trigger_type = WebhookTriggerTypeField(required=False) class Meta(WebhookCreateSerializer.Meta): extra_kwargs = { "name": {"required": False, "allow_null": False, "allow_blank": False}, "is_webhook_enabled": {"required": False, "allow_null": False}, "username": {"required": False, "allow_null": True, "allow_blank": True}, "password": {"required": False, "allow_null": True, "allow_blank": True}, "authorization_header": {"required": False, "allow_null": True, "allow_blank": True}, "trigger_template": {"required": False, "allow_null": True, "allow_blank": True}, "headers": {"required": False, "allow_null": True, "allow_blank": True}, "url": {"required": False, "allow_null": False, "allow_blank": False}, "data": {"required": False, "allow_null": True, "allow_blank": True}, "forward_all": {"required": False, "allow_null": False}, "http_method": {"required": False, "allow_null": False, "allow_blank": False}, }