diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/api/serializers/module.py index 81d07f884..65710e8af 100644 --- a/apiserver/plane/api/serializers/module.py +++ b/apiserver/plane/api/serializers/module.py @@ -55,7 +55,6 @@ class ModuleSerializer(BaseSerializer): 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"], diff --git a/apiserver/plane/api/urls/inbox.py b/apiserver/plane/api/urls/inbox.py index 884676cbc..3a2a57786 100644 --- a/apiserver/plane/api/urls/inbox.py +++ b/apiserver/plane/api/urls/inbox.py @@ -10,7 +10,7 @@ urlpatterns = [ name="inbox-issue", ), path( - "workspaces//projects//inbox-issues//", + "workspaces//projects//inbox-issues//", InboxIssueAPIEndpoint.as_view(), name="inbox-issue", ), diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 5217c32e5..94ddc4f10 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -60,9 +60,9 @@ class InboxIssueAPIEndpoint(BaseAPIView): .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, @@ -163,7 +163,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): 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() @@ -184,7 +184,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): # 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, @@ -212,7 +212,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): 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: @@ -236,7 +236,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): 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, @@ -261,7 +261,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): # 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, ) @@ -275,7 +275,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): # 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, ) @@ -297,7 +297,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): 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() @@ -318,7 +318,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): # 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, @@ -345,7 +345,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): 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() diff --git a/apiserver/plane/app/serializers/webhook.py b/apiserver/plane/app/serializers/webhook.py index d5b3eeddd..961466d28 100644 --- a/apiserver/plane/app/serializers/webhook.py +++ b/apiserver/plane/app/serializers/webhook.py @@ -14,9 +14,9 @@ from plane.db.models.webhook import validate_domain, validate_schema 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 @@ -48,8 +48,42 @@ class WebhookSerializer(DynamicBaseSerializer): 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 diff --git a/apiserver/plane/bgtasks/event_tracking_task.py b/apiserver/plane/bgtasks/event_tracking_task.py index e8b5b7831..2e579bca1 100644 --- a/apiserver/plane/bgtasks/event_tracking_task.py +++ b/apiserver/plane/bgtasks/event_tracking_task.py @@ -10,7 +10,6 @@ from sentry_sdk import capture_exception @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( diff --git a/apiserver/plane/bgtasks/webhook_task.py b/apiserver/plane/bgtasks/webhook_task.py index c9e0ddceb..f5ee96256 100644 --- a/apiserver/plane/bgtasks/webhook_task.py +++ b/apiserver/plane/bgtasks/webhook_task.py @@ -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", @@ -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, diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index b99f74c84..b8998a87e 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -240,7 +240,7 @@ if AWS_S3_ENDPOINT_URL: # 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,