diff --git a/src/olympia/lib/settings_base.py b/src/olympia/lib/settings_base.py index 16767d0942a1..1d0ff6c24d9a 100644 --- a/src/olympia/lib/settings_base.py +++ b/src/olympia/lib/settings_base.py @@ -833,6 +833,7 @@ def get_language_url_map(): 'olympia.scanners.tasks.run_customs': {'queue': 'devhub'}, 'olympia.scanners.tasks.run_narc_on_version': {'queue': 'devhub'}, 'olympia.scanners.tasks.run_yara': {'queue': 'devhub'}, + 'olympia.versions.tasks.call_source_builder': {'queue': 'devhub'}, 'olympia.versions.tasks.soft_block_versions': {'queue': 'devhub'}, # Crons. 'olympia.addons.tasks.update_addon_average_daily_users': {'queue': 'cron'}, @@ -1557,3 +1558,8 @@ def read_only_mode(env): SWAGGER_SCHEMA_FILE = path('schema.yml') SWAGGER_UI_ENABLED = env('SWAGGER_UI_ENABLED', default=False) or TARGET != 'production' + +# Source builder settings. +SOURCE_BUILDER_API_URL = env('SOURCE_BUILDER_API_URL', default=None) +SOURCE_BUILDER_API_KEY = env('SOURCE_BUILDER_API_KEY', default='this-is-a-dummy-key') +SOURCE_BUILDER_API_TIMEOUT = 5 # seconds diff --git a/src/olympia/versions/migrations/0051_create_enable_source_builder_switch.py b/src/olympia/versions/migrations/0051_create_enable_source_builder_switch.py new file mode 100644 index 000000000000..bb7873491f0c --- /dev/null +++ b/src/olympia/versions/migrations/0051_create_enable_source_builder_switch.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.26 on 2025-11-13 08:55 + +from django.db import migrations +from olympia.core.db.migrations import CreateWaffleSwitch + + +class Migration(migrations.Migration): + + dependencies = [ + ('versions', '0050_remove_versionreviewerflags_needs_human_review_by_mad'), + ] + + operations = [CreateWaffleSwitch('enable-source-builder')] diff --git a/src/olympia/versions/models.py b/src/olympia/versions/models.py index d48e1aac0754..1a2458b0b0d9 100644 --- a/src/olympia/versions/models.py +++ b/src/olympia/versions/models.py @@ -826,6 +826,8 @@ def flag_if_sources_were_provided(self, user): from olympia.activity.utils import log_and_notify from olympia.reviewers.models import NeedsHumanReview + from .tasks import call_source_builder + if self.source: # Add Activity Log, notifying staff, relevant reviewers and # other authors of the add-on. @@ -835,6 +837,9 @@ def flag_if_sources_were_provided(self, user): reason = NeedsHumanReview.REASONS.PENDING_REJECTION_SOURCES_PROVIDED NeedsHumanReview.objects.create(version=self, reason=reason) + if waffle.switch_is_active('enable-source-builder'): + call_source_builder.delay(version_pk=self.pk) + @classmethod def transformer(cls, versions): """Attach all the compatible apps and the file to the versions.""" diff --git a/src/olympia/versions/tasks.py b/src/olympia/versions/tasks.py index 915ec98bddaa..e8b6688795d5 100644 --- a/src/olympia/versions/tasks.py +++ b/src/olympia/versions/tasks.py @@ -3,11 +3,14 @@ import os import tempfile from io import BytesIO +from urllib.parse import urljoin from django.conf import settings from django.db import transaction from django.template import loader +from django.urls import reverse +import requests from django_statsd.clients import statsd from PIL import Image @@ -24,6 +27,7 @@ from olympia.files.utils import get_background_images from olympia.lib.crypto.tasks import duplicate_addon_version from olympia.reviewers.models import NeedsHumanReview +from olympia.scanners.tasks import make_adapter_with_retry from olympia.users.models import UserProfile from olympia.users.utils import get_task_user from olympia.versions.compare import VersionString @@ -416,3 +420,35 @@ def soft_block_versions(version_ids, reason=REASON_VERSION_DELETED, **kw): ), overwrite_block_metadata=False, ) + + +@task +def call_source_builder(version_pk): + log.info('Calling source builder API for Version %s', version_pk) + + try: + version = Version.objects.get(pk=version_pk) + + with requests.Session() as http: + adapter = make_adapter_with_retry() + http.mount('http://', adapter) + http.mount('https://', adapter) + + json_payload = { + 'addon_id': version.addon_id, + 'version_id': version.id, + 'download_source_url': urljoin( + settings.EXTERNAL_SITE_URL, + reverse('downloads.source', kwargs={'version_id': version.id}), + ), + 'license_slug': version.license.slug, + } + http.post( + url=settings.SOURCE_BUILDER_API_URL, + json=json_payload, + timeout=settings.SOURCE_BUILDER_API_TIMEOUT, + ) + except Exception: + log.exception( + 'Error while calling source builder API for Version %s.', version_pk + ) diff --git a/src/olympia/versions/views.py b/src/olympia/versions/views.py index 167d9edfaddc..b3dd6e1f9e42 100644 --- a/src/olympia/versions/views.py +++ b/src/olympia/versions/views.py @@ -1,11 +1,15 @@ import os from django import http +from django.conf import settings from django.db.transaction import non_atomic_requests from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse from django.utils.cache import patch_cache_control, patch_vary_headers +from django.utils.crypto import constant_time_compare + +import waffle import olympia.core.logger from olympia import amo @@ -218,6 +222,18 @@ def download_source(request, version_id): allow_addons_edit_permission=False, allow_developer=True, ) + + # Source code can also be downloaded using an API key when the source + # builder feature is enabled. + api_key = request.headers.get('x-api-key') + if waffle.switch_is_active('enable-source-builder') and api_key: + has_permission = constant_time_compare(api_key, settings.SOURCE_BUILDER_API_KEY) + if has_permission: + log.info( + 'Source code for Version %s was accessed via SOURCE_BUILDER_API_KEY', + version.id, + ) + if not (has_permission and getattr(version, 'source', None)): raise http.Http404()