Skip to content

Commit c3fb3a9

Browse files
committed
Merge tag '10.15.0' into relcorp
2 parents 527a4fb + 9e2e786 commit c3fb3a9

File tree

103 files changed

+2937
-2160
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

103 files changed

+2937
-2160
lines changed

CHANGELOG.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
Version 10.15.0
2+
---------------
3+
4+
:Date: January 09, 2024
5+
6+
* `@humitos <https://github.com/humitos>`__: pip-compile fixes (`#11010 <https://github.com/readthedocs/readthedocs.org/pull/11010>`__)
7+
* `@github-actions[bot] <https://github.com/github-actions[bot]>`__: Dependencies: all packages updated via pip-tools (`#11005 <https://github.com/readthedocs/readthedocs.org/pull/11005>`__)
8+
* `@ericholscher <https://github.com/ericholscher>`__: Fix structlog by downgrading it (`#11003 <https://github.com/readthedocs/readthedocs.org/pull/11003>`__)
9+
* `@humitos <https://github.com/humitos>`__: Eslint fix issues (`#10998 <https://github.com/readthedocs/readthedocs.org/pull/10998>`__)
10+
* `@webknjaz <https://github.com/webknjaz>`__: Fix ref to the "new addons integrations" blog post @ custom build doc (`#10997 <https://github.com/readthedocs/readthedocs.org/pull/10997>`__)
11+
* `@humitos <https://github.com/humitos>`__: Notifications: small fixes found after reviewer (`#10996 <https://github.com/readthedocs/readthedocs.org/pull/10996>`__)
12+
* `@stsewd <https://github.com/stsewd>`__: Update common (`#10995 <https://github.com/readthedocs/readthedocs.org/pull/10995>`__)
13+
* `@humitos <https://github.com/humitos>`__: Remove leftovers from `django-messages-extends` (`#10994 <https://github.com/readthedocs/readthedocs.org/pull/10994>`__)
14+
* `@stsewd <https://github.com/stsewd>`__: Integrations: hardcode deprecation date for incoming webhooks without a secret (`#10993 <https://github.com/readthedocs/readthedocs.org/pull/10993>`__)
15+
* `@stsewd <https://github.com/stsewd>`__: Development: update steps for testing subscriptions (`#10992 <https://github.com/readthedocs/readthedocs.org/pull/10992>`__)
16+
* `@stsewd <https://github.com/stsewd>`__: Redirects: remove null option from position field (`#10991 <https://github.com/readthedocs/readthedocs.org/pull/10991>`__)
17+
* `@ericholscher <https://github.com/ericholscher>`__: Release 10.14.0 (`#10989 <https://github.com/readthedocs/readthedocs.org/pull/10989>`__)
18+
* `@humitos <https://github.com/humitos>`__: Addons: get translation from main project (`#10952 <https://github.com/readthedocs/readthedocs.org/pull/10952>`__)
19+
* `@humitos <https://github.com/humitos>`__: New notification system: implementation (`#10922 <https://github.com/readthedocs/readthedocs.org/pull/10922>`__)
20+
* `@stsewd <https://github.com/stsewd>`__: Custom domains: don't allow adding a custom domain on subprojects (`#8953 <https://github.com/readthedocs/readthedocs.org/pull/8953>`__)
21+
122
Version 10.14.0
223
---------------
324

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777

7878
master_doc = "index"
7979
copyright = "Read the Docs, Inc & contributors"
80-
version = "10.14.0"
80+
version = "10.15.0"
8181
release = version
8282
exclude_patterns = ["_build", "shared", "_includes"]
8383
default_role = "obj"

docs/dev/subscriptions.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ Local testing
99
-------------
1010

1111
To test subscriptions locally, you need to have access to the Stripe account,
12-
and define the following settings with the keys from Stripe test mode:
12+
and define the following environment variables with the keys from Stripe test mode:
1313

14-
- ``STRIPE_SECRET``: https://dashboard.stripe.com/test/apikeys
15-
- ``STRIPE_TEST_SECRET_KEY``: https://dashboard.stripe.com/test/apikeys
16-
- ``DJSTRIPE_WEBHOOK_SECRET``: https://dashboard.stripe.com/test/webhooks
14+
- ``RTD_STRIPE_SECRET``: https://dashboard.stripe.com/test/apikeys
15+
- ``RTD_DJSTRIPE_WEBHOOK_SECRET``: https://dashboard.stripe.com/test/webhooks
1716

1817
To test the webhook locally, you need to run your local instance with ngrok, for example:
1918

docs/user/build-customization.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,7 @@ Override the build process
390390
.. warning::
391391

392392
This feature is in *beta* and could change without warning.
393-
We are currently testing `the new addons integrations we are building <rtd-blog:addons-flyout-menu-beta>`_
393+
We are currently testing :ref:`the new addons integrations we are building <rtd-blog:addons-flyout-menu-beta>`
394394
on projects using ``build.commands`` configuration key.
395395

396396
If your project requires full control of the build process,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "readthedocs",
3-
"version": "10.14.0",
3+
"version": "10.15.0",
44
"description": "Read the Docs build dependencies",
55
"author": "Read the Docs, Inc <[email protected]>",
66
"scripts": {

readthedocs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""Read the Docs."""
22

33

4-
__version__ = "10.14.0"
4+
__version__ = "10.15.0"

readthedocs/api/v2/serializers.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33

44
from allauth.socialaccount.models import SocialAccount
5+
from django.core.exceptions import ObjectDoesNotExist
6+
from django.utils.translation import gettext as _
7+
from generic_relations.relations import GenericRelatedField
58
from rest_framework import serializers
69

710
from readthedocs.api.v2.utils import normalize_build_command
811
from readthedocs.builds.models import Build, BuildCommandResult, Version
912
from readthedocs.core.resolver import Resolver
13+
from readthedocs.notifications.models import Notification
1014
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
1115
from readthedocs.projects.models import Domain, Project
1216

@@ -378,3 +382,53 @@ def get_username(self, obj):
378382
or obj.extra_data.get("login")
379383
# FIXME: which one is GitLab?
380384
)
385+
386+
387+
class NotificationAttachedToRelatedField(serializers.RelatedField):
388+
389+
"""
390+
Attached to related field for Notifications.
391+
392+
Used together with ``rest-framework-generic-relations`` to accept multiple object types on ``attached_to``.
393+
394+
See https://github.com/LilyFoote/rest-framework-generic-relations
395+
"""
396+
397+
default_error_messages = {
398+
"required": _("This field is required."),
399+
"does_not_exist": _("Object does not exist."),
400+
"incorrect_type": _(
401+
"Incorrect type. Expected URL string, received {data_type}."
402+
),
403+
}
404+
405+
def to_representation(self, value):
406+
return f"{self.queryset.model._meta.model_name}/{value.pk}"
407+
408+
def to_internal_value(self, data):
409+
# TODO: handle exceptions
410+
model, pk = data.strip("/").split("/")
411+
if self.queryset.model._meta.model_name != model:
412+
self.fail("incorrect_type")
413+
414+
try:
415+
return self.queryset.get(pk=pk)
416+
except (ObjectDoesNotExist, ValueError, TypeError):
417+
self.fail("does_not_exist")
418+
419+
420+
class NotificationSerializer(serializers.ModelSerializer):
421+
# Accept different object types (Project, Build, User, etc) depending on what the notification is attached to.
422+
# The client has to send a value like "<model>/<pk>".
423+
# Example: "build/3522" will attach the notification to the Build object with id 3522
424+
attached_to = GenericRelatedField(
425+
{
426+
Build: NotificationAttachedToRelatedField(queryset=Build.objects.all()),
427+
Project: NotificationAttachedToRelatedField(queryset=Project.objects.all()),
428+
},
429+
required=True,
430+
)
431+
432+
class Meta:
433+
model = Notification
434+
exclude = ["attached_to_id", "attached_to_content_type"]

readthedocs/api/v2/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
BuildCommandViewSet,
1313
BuildViewSet,
1414
DomainViewSet,
15+
NotificationViewSet,
1516
ProjectViewSet,
1617
RemoteOrganizationViewSet,
1718
RemoteRepositoryViewSet,
@@ -25,6 +26,7 @@
2526
router.register(r"version", VersionViewSet, basename="version")
2627
router.register(r"project", ProjectViewSet, basename="project")
2728
router.register(r"domain", DomainViewSet, basename="domain")
29+
router.register(r"notifications", NotificationViewSet, basename="notifications")
2830
router.register(
2931
r"remote/org",
3032
RemoteOrganizationViewSet,

readthedocs/api/v2/views/integrations.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Endpoints integrating with Github, Bitbucket, and other webhooks."""
22

3+
import datetime
34
import hashlib
45
import hmac
56
import json
@@ -9,6 +10,7 @@
910

1011
import structlog
1112
from django.shortcuts import get_object_or_404
13+
from django.utils import timezone
1214
from django.utils.crypto import constant_time_compare
1315
from rest_framework import permissions, status
1416
from rest_framework.exceptions import NotFound, ParseError
@@ -74,9 +76,17 @@ class WebhookMixin:
7476
invalid_payload_msg = 'Payload not valid'
7577
missing_secret_for_pr_events_msg = dedent(
7678
"""
77-
The webhook doesn't have a secret configured.
79+
This webhook doesn't have a secret configured.
7880
For security reasons, webhooks without a secret can't process pull/merge request events.
79-
You can read more information about this in our blog post: https://blog.readthedocs.com/security-update-on-incoming-webhooks/.
81+
For more information, read our blog post: https://blog.readthedocs.com/security-update-on-incoming-webhooks/.
82+
"""
83+
).strip()
84+
85+
missing_secret_deprecated_msg = dedent(
86+
"""
87+
This webhook doesn't have a secret configured.
88+
For security reasons, webhooks without a secret are no longer permitted.
89+
For more information, read our blog post: https://blog.readthedocs.com/security-update-on-incoming-webhooks/.
8090
"""
8191
).strip()
8292

@@ -105,6 +115,18 @@ def post(self, request, project_slug):
105115
return Response(resp, status=status.HTTP_406_NOT_ACCEPTABLE)
106116
except Project.DoesNotExist as exc:
107117
raise NotFound("Project not found") from exc
118+
119+
# Deprecate webhooks without a secret
120+
# https://blog.readthedocs.com/security-update-on-incoming-webhooks/.
121+
now = timezone.now()
122+
deprecation_date = datetime.datetime(2024, 1, 31, tzinfo=datetime.timezone.utc)
123+
is_deprecated = now >= deprecation_date
124+
if is_deprecated and not self.has_secret():
125+
return Response(
126+
{"detail": self.missing_secret_deprecated_msg},
127+
status=HTTP_400_BAD_REQUEST,
128+
)
129+
108130
if not self.is_payload_valid():
109131
log.warning('Invalid payload for project and integration.')
110132
return Response(
@@ -122,6 +144,12 @@ def post(self, request, project_slug):
122144
return resp
123145
return Response(resp)
124146

147+
def has_secret(self):
148+
integration = self.get_integration()
149+
if hasattr(integration, "token"):
150+
return bool(integration.token)
151+
return bool(integration.secret)
152+
125153
def get_project(self, **kwargs):
126154
return Project.objects.get(**kwargs)
127155

readthedocs/api/v2/views/model_views.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from readthedocs.api.v2.utils import normalize_build_command
1919
from readthedocs.builds.constants import INTERNAL
2020
from readthedocs.builds.models import Build, BuildCommandResult, Version
21+
from readthedocs.notifications.models import Notification
2122
from readthedocs.oauth.models import RemoteOrganization, RemoteRepository
2223
from readthedocs.oauth.services import registry
2324
from readthedocs.projects.models import Domain, Project
@@ -29,6 +30,7 @@
2930
BuildCommandSerializer,
3031
BuildSerializer,
3132
DomainSerializer,
33+
NotificationSerializer,
3234
ProjectAdminSerializer,
3335
ProjectSerializer,
3436
RemoteOrganizationSerializer,
@@ -361,6 +363,42 @@ def get_queryset_for_api_key(self, api_key):
361363
return self.model.objects.filter(build__project=api_key.project)
362364

363365

366+
class NotificationViewSet(DisableListEndpoint, CreateModelMixin, UserSelectViewSet):
367+
368+
"""
369+
Create a notification attached to an object (User, Project, Build, Organization).
370+
371+
This endpoint is currently used only internally by the builder.
372+
Notifications are attached to `Build` objects only when using this endpoint.
373+
This limitation will change in the future when re-implementing this on APIv3 if neeed.
374+
"""
375+
376+
parser_classes = [JSONParser, MultiPartParser]
377+
permission_classes = [HasBuildAPIKey]
378+
renderer_classes = (JSONRenderer,)
379+
serializer_class = NotificationSerializer
380+
model = Notification
381+
382+
def perform_create(self, serializer):
383+
"""Restrict creation to notifications attached to the project's builds from the api key."""
384+
attached_to = serializer.validated_data["attached_to"]
385+
386+
build_api_key = self.request.build_api_key
387+
388+
project_slug = None
389+
if isinstance(attached_to, Build):
390+
project_slug = attached_to.project.slug
391+
elif isinstance(attached_to, Project):
392+
project_slug = attached_to.slug
393+
394+
# Limit the permissions to create a notification on this object only if the API key
395+
# is attached to the related project
396+
if not project_slug or build_api_key.project.slug != project_slug:
397+
raise PermissionDenied()
398+
399+
return super().perform_create(serializer)
400+
401+
364402
class DomainViewSet(DisableListEndpoint, UserSelectViewSet):
365403
permission_classes = [ReadOnlyPermission]
366404
renderer_classes = (JSONRenderer,)

0 commit comments

Comments
 (0)