forked from github/plane
chore: api webhooks validation (#2928)
* chore: api webhooks update * chore: webhooks signature validation
This commit is contained in:
parent
cdc4fd27a5
commit
6a4f521b47
@ -55,7 +55,6 @@ class ModuleSerializer(BaseSerializer):
|
|||||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||||
|
|
||||||
if data.get("members", []):
|
if data.get("members", []):
|
||||||
print(data.get("members"))
|
|
||||||
data["members"] = ProjectMember.objects.filter(
|
data["members"] = ProjectMember.objects.filter(
|
||||||
project_id=self.context.get("project_id"),
|
project_id=self.context.get("project_id"),
|
||||||
member_id__in=data["members"],
|
member_id__in=data["members"],
|
||||||
|
@ -10,7 +10,7 @@ urlpatterns = [
|
|||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:pk>/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/inbox-issues/<uuid:issue_id>/",
|
||||||
InboxIssueAPIEndpoint.as_view(),
|
InboxIssueAPIEndpoint.as_view(),
|
||||||
name="inbox-issue",
|
name="inbox-issue",
|
||||||
),
|
),
|
||||||
|
@ -60,9 +60,9 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
.order_by(self.kwargs.get("order_by", "-created_at"))
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, request, slug, project_id, pk=None):
|
def get(self, request, slug, project_id, issue_id=None):
|
||||||
if pk:
|
if issue_id:
|
||||||
inbox_issue_queryset = self.get_queryset().get(pk=pk)
|
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
|
||||||
inbox_issue_data = InboxIssueSerializer(
|
inbox_issue_data = InboxIssueSerializer(
|
||||||
inbox_issue_queryset,
|
inbox_issue_queryset,
|
||||||
fields=self.fields,
|
fields=self.fields,
|
||||||
@ -163,7 +163,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
serializer = InboxIssueSerializer(inbox_issue)
|
serializer = InboxIssueSerializer(inbox_issue)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
def patch(self, request, slug, project_id, pk):
|
def patch(self, request, slug, project_id, issue_id):
|
||||||
inbox = Inbox.objects.filter(
|
inbox = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
).first()
|
).first()
|
||||||
@ -184,7 +184,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# Get the inbox issue
|
# Get the inbox issue
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk,
|
issue_id=issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
inbox_id=inbox.id,
|
inbox_id=inbox.id,
|
||||||
@ -212,7 +212,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
if bool(issue_data):
|
if bool(issue_data):
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
pk=issue_id, workspace__slug=slug, project_id=project_id
|
||||||
)
|
)
|
||||||
# Only allow guests and viewers to edit name and description
|
# Only allow guests and viewers to edit name and description
|
||||||
if project_member.role <= 10:
|
if project_member.role <= 10:
|
||||||
@ -236,7 +236,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
type="issue.activity.updated",
|
type="issue.activity.updated",
|
||||||
requested_data=requested_data,
|
requested_data=requested_data,
|
||||||
actor_id=str(request.user.id),
|
actor_id=str(request.user.id),
|
||||||
issue_id=str(issue.id),
|
issue_id=str(issue_id),
|
||||||
project_id=str(project_id),
|
project_id=str(project_id),
|
||||||
current_instance=json.dumps(
|
current_instance=json.dumps(
|
||||||
IssueSerializer(current_instance).data,
|
IssueSerializer(current_instance).data,
|
||||||
@ -261,7 +261,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
# Update the issue state if the issue is rejected or marked as duplicate
|
# Update the issue state if the issue is rejected or marked as duplicate
|
||||||
if serializer.data["status"] in [-1, 2]:
|
if serializer.data["status"] in [-1, 2]:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id,
|
pk=issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
@ -275,7 +275,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
# Update the issue state if it is accepted
|
# Update the issue state if it is accepted
|
||||||
if serializer.data["status"] in [1]:
|
if serializer.data["status"] in [1]:
|
||||||
issue = Issue.objects.get(
|
issue = Issue.objects.get(
|
||||||
pk=inbox_issue.issue_id,
|
pk=issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
@ -297,7 +297,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
InboxIssueSerializer(inbox_issue).data, status=status.HTTP_200_OK
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self, request, slug, project_id, pk):
|
def delete(self, request, slug, project_id, issue_id):
|
||||||
inbox = Inbox.objects.filter(
|
inbox = Inbox.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id
|
workspace__slug=slug, project_id=project_id
|
||||||
).first()
|
).first()
|
||||||
@ -318,7 +318,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
|
|
||||||
# Get the inbox issue
|
# Get the inbox issue
|
||||||
inbox_issue = InboxIssue.objects.get(
|
inbox_issue = InboxIssue.objects.get(
|
||||||
pk=pk,
|
issue_id=issue_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
inbox_id=inbox.id,
|
inbox_id=inbox.id,
|
||||||
@ -345,7 +345,7 @@ class InboxIssueAPIEndpoint(BaseAPIView):
|
|||||||
if inbox_issue.status in [-2, -1, 0, 2]:
|
if inbox_issue.status in [-2, -1, 0, 2]:
|
||||||
# Delete the issue also
|
# Delete the issue also
|
||||||
Issue.objects.filter(
|
Issue.objects.filter(
|
||||||
workspace__slug=slug, project_id=project_id, pk=inbox_issue.issue_id
|
workspace__slug=slug, project_id=project_id, pk=issue_id
|
||||||
).delete()
|
).delete()
|
||||||
|
|
||||||
inbox_issue.delete()
|
inbox_issue.delete()
|
||||||
|
@ -15,8 +15,8 @@ from plane.db.models.webhook import validate_domain, validate_schema
|
|||||||
class WebhookSerializer(DynamicBaseSerializer):
|
class WebhookSerializer(DynamicBaseSerializer):
|
||||||
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
url = serializers.URLField(validators=[validate_schema, validate_domain])
|
||||||
|
|
||||||
def validate(self, data):
|
def create(self, validated_data):
|
||||||
url = data.get("url", None)
|
url = validated_data.get("url", None)
|
||||||
|
|
||||||
# Extract the hostname from the URL
|
# Extract the hostname from the URL
|
||||||
hostname = urlparse(url).hostname
|
hostname = urlparse(url).hostname
|
||||||
@ -48,8 +48,42 @@ class WebhookSerializer(DynamicBaseSerializer):
|
|||||||
if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
|
if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
|
||||||
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
||||||
|
|
||||||
return data
|
return Webhook.objects.create(**validated_data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
url = validated_data.get("url", None)
|
||||||
|
if url:
|
||||||
|
# Extract the hostname from the URL
|
||||||
|
hostname = urlparse(url).hostname
|
||||||
|
if not hostname:
|
||||||
|
raise serializers.ValidationError({"url": "Invalid URL: No hostname found."})
|
||||||
|
|
||||||
|
# Resolve the hostname to IP addresses
|
||||||
|
try:
|
||||||
|
ip_addresses = socket.getaddrinfo(hostname, None)
|
||||||
|
except socket.gaierror:
|
||||||
|
raise serializers.ValidationError({"url": "Hostname could not be resolved."})
|
||||||
|
|
||||||
|
if not ip_addresses:
|
||||||
|
raise serializers.ValidationError({"url": "No IP addresses found for the hostname."})
|
||||||
|
|
||||||
|
for addr in ip_addresses:
|
||||||
|
ip = ipaddress.ip_address(addr[4][0])
|
||||||
|
if ip.is_private or ip.is_loopback:
|
||||||
|
raise serializers.ValidationError({"url": "URL resolves to a blocked IP address."})
|
||||||
|
|
||||||
|
# Additional validation for multiple request domains and their subdomains
|
||||||
|
request = self.context.get('request')
|
||||||
|
disallowed_domains = ['plane.so',] # Add your disallowed domains here
|
||||||
|
if request:
|
||||||
|
request_host = request.get_host().split(':')[0] # Remove port if present
|
||||||
|
disallowed_domains.append(request_host)
|
||||||
|
|
||||||
|
# Check if hostname is a subdomain or exact match of any disallowed domain
|
||||||
|
if any(hostname == domain or hostname.endswith('.' + domain) for domain in disallowed_domains):
|
||||||
|
raise serializers.ValidationError({"url": "URL domain or its subdomain is not allowed."})
|
||||||
|
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Webhook
|
model = Webhook
|
||||||
|
@ -10,7 +10,6 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
def auth_events(user, email, user_agent, ip, event_name, medium, first_time):
|
||||||
print(user, email, user_agent, ip, event_name, medium, first_time)
|
|
||||||
try:
|
try:
|
||||||
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST)
|
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST)
|
||||||
posthog.capture(
|
posthog.capture(
|
||||||
|
@ -90,17 +90,6 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use HMAC for generating signature
|
|
||||||
if webhook.secret_key:
|
|
||||||
event_data_json = json.dumps(event_data) if event_data is not None else "{}"
|
|
||||||
hmac_signature = hmac.new(
|
|
||||||
webhook.secret_key.encode("utf-8"),
|
|
||||||
event_data_json.encode("utf-8"),
|
|
||||||
hashlib.sha256,
|
|
||||||
)
|
|
||||||
signature = hmac_signature.hexdigest()
|
|
||||||
headers["X-Plane-Signature"] = signature
|
|
||||||
|
|
||||||
action = {
|
action = {
|
||||||
"POST": "create",
|
"POST": "create",
|
||||||
"PATCH": "update",
|
"PATCH": "update",
|
||||||
@ -116,6 +105,16 @@ def webhook_task(self, webhook, slug, event, event_data, action):
|
|||||||
"data": event_data,
|
"data": event_data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Use HMAC for generating signature
|
||||||
|
if webhook.secret_key:
|
||||||
|
hmac_signature = hmac.new(
|
||||||
|
webhook.secret_key.encode("utf-8"),
|
||||||
|
json.dumps(payload, sort_keys=True).encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
)
|
||||||
|
signature = hmac_signature.hexdigest()
|
||||||
|
headers["X-Plane-Signature"] = signature
|
||||||
|
|
||||||
# Send the webhook event
|
# Send the webhook event
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
webhook.url,
|
webhook.url,
|
||||||
|
@ -240,7 +240,7 @@ if AWS_S3_ENDPOINT_URL:
|
|||||||
|
|
||||||
# JWT Auth Configuration
|
# JWT Auth Configuration
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080),
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=43200),
|
||||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=43200),
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=43200),
|
||||||
"ROTATE_REFRESH_TOKENS": False,
|
"ROTATE_REFRESH_TOKENS": False,
|
||||||
"BLACKLIST_AFTER_ROTATION": False,
|
"BLACKLIST_AFTER_ROTATION": False,
|
||||||
|
Loading…
Reference in New Issue
Block a user