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).
199 lines
8.1 KiB
Python
199 lines
8.1 KiB
Python
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},
|
|
}
|