Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: api webhooks validation #2928

Merged
merged 3 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apiserver/plane/api/serializers/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ def validate(self, data):
raise serializers.ValidationError("Start date cannot exceed target date")

if data.get("members", []):
print(data.get("members"))
data["members"] = ProjectMember.objects.filter(
project_id=self.context.get("project_id"),
member_id__in=data["members"],
Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/api/urls/inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
name="inbox-issue",
),
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(),
name="inbox-issue",
),
Expand Down
24 changes: 12 additions & 12 deletions apiserver/plane/api/views/inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ def get_queryset(self):
.order_by(self.kwargs.get("order_by", "-created_at"))
)

def get(self, request, slug, project_id, pk=None):
if pk:
inbox_issue_queryset = self.get_queryset().get(pk=pk)
def get(self, request, slug, project_id, issue_id=None):
if issue_id:
inbox_issue_queryset = self.get_queryset().get(issue_id=issue_id)
inbox_issue_data = InboxIssueSerializer(
inbox_issue_queryset,
fields=self.fields,
Expand Down Expand Up @@ -163,7 +163,7 @@ def post(self, request, slug, project_id):
serializer = InboxIssueSerializer(inbox_issue)
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(
workspace__slug=slug, project_id=project_id
).first()
Expand All @@ -184,7 +184,7 @@ def patch(self, request, slug, project_id, pk):

# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
pk=pk,
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox.id,
Expand Down Expand Up @@ -212,7 +212,7 @@ def patch(self, request, slug, project_id, pk):

if bool(issue_data):
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
if project_member.role <= 10:
Expand All @@ -236,7 +236,7 @@ def patch(self, request, slug, project_id, pk):
type="issue.activity.updated",
requested_data=requested_data,
actor_id=str(request.user.id),
issue_id=str(issue.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=json.dumps(
IssueSerializer(current_instance).data,
Expand All @@ -261,7 +261,7 @@ def patch(self, request, slug, project_id, pk):
# Update the issue state if the issue is rejected or marked as duplicate
if serializer.data["status"] in [-1, 2]:
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
Expand All @@ -275,7 +275,7 @@ def patch(self, request, slug, project_id, pk):
# Update the issue state if it is accepted
if serializer.data["status"] in [1]:
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
pk=issue_id,
workspace__slug=slug,
project_id=project_id,
)
Expand All @@ -297,7 +297,7 @@ def patch(self, request, slug, project_id, pk):
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(
workspace__slug=slug, project_id=project_id
).first()
Expand All @@ -318,7 +318,7 @@ def delete(self, request, slug, project_id, pk):

# Get the inbox issue
inbox_issue = InboxIssue.objects.get(
pk=pk,
issue_id=issue_id,
workspace__slug=slug,
project_id=project_id,
inbox_id=inbox.id,
Expand All @@ -345,7 +345,7 @@ def delete(self, request, slug, project_id, pk):
if inbox_issue.status in [-2, -1, 0, 2]:
# Delete the issue also
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()

inbox_issue.delete()
Expand Down
44 changes: 39 additions & 5 deletions apiserver/plane/app/serializers/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

class WebhookSerializer(DynamicBaseSerializer):
url = serializers.URLField(validators=[validate_schema, validate_domain])

def validate(self, data):
url = data.get("url", None)
def create(self, validated_data):
url = validated_data.get("url", None)

# Extract the hostname from the URL
hostname = urlparse(url).hostname
Expand Down Expand Up @@ -48,8 +48,42 @@ def validate(self, data):
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 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:
model = Webhook
Expand Down
1 change: 0 additions & 1 deletion apiserver/plane/bgtasks/event_tracking_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

@shared_task
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:
posthog = Posthog(settings.POSTHOG_API_KEY, host=settings.POSTHOG_HOST)
posthog.capture(
Expand Down
21 changes: 10 additions & 11 deletions apiserver/plane/bgtasks/webhook_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,6 @@ def webhook_task(self, webhook, slug, event, event_data, action):
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 = {
"POST": "create",
"PATCH": "update",
Expand All @@ -116,6 +105,16 @@ def webhook_task(self, webhook, slug, event, event_data, action):
"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
response = requests.post(
webhook.url,
Expand Down
2 changes: 1 addition & 1 deletion apiserver/plane/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@

# JWT Auth Configuration
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=10080),
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=43200),
"REFRESH_TOKEN_LIFETIME": timedelta(days=43200),
"ROTATE_REFRESH_TOKENS": False,
"BLACKLIST_AFTER_ROTATION": False,
Expand Down
Loading