From 60a3abc815ecc0ced5e2f1b060e5d7ef487923c2 Mon Sep 17 00:00:00 2001 From: Stephen Herr Date: Mon, 28 Nov 2022 17:18:37 -0500 Subject: [PATCH] Specify and remember the Signing Service that should be used at Repository or Release creation time. closes #641 --- CHANGES/641.feature | 1 + docs/feature_overview.rst | 7 +- .../0023_add_default_signing_services.py | 36 +++++++ pulp_deb/app/models/__init__.py | 6 +- pulp_deb/app/models/repository.py | 38 +++++++- .../app/serializers/repository_serializers.py | 96 ++++++++++++++++++- pulp_deb/app/tasks/publishing.py | 8 +- pulp_deb/tests/functional/api/test_publish.py | 85 +++++++++++++++- 8 files changed, 266 insertions(+), 11 deletions(-) create mode 100644 CHANGES/641.feature create mode 100644 pulp_deb/app/migrations/0023_add_default_signing_services.py diff --git a/CHANGES/641.feature b/CHANGES/641.feature new file mode 100644 index 000000000..bfddd810c --- /dev/null +++ b/CHANGES/641.feature @@ -0,0 +1 @@ +Specify and remember the Signing Services we want to use for each Repo / Release. \ No newline at end of file diff --git a/docs/feature_overview.rst b/docs/feature_overview.rst index ead5d7056..8616441b0 100644 --- a/docs/feature_overview.rst +++ b/docs/feature_overview.rst @@ -187,7 +187,12 @@ The short version: ``/pulp/api/v3/signing-services/`` -6. And use it when you Publish content. ``pulp_deb`` will call out to your script to sign the ``Release`` file and publish the signatures as part of the Publish action. +6. And use it when you Publish content. ``pulp_deb`` will call out to your script to sign the ``Release`` file and publish the signatures as part of the Publish action. The three ways you can specify the Signing Service are: + + 1. If you specify ``signing_service`` when creating the Publication, that service will sign all the Releases in the Publication. + 2. You can specify that particular Release in a Repository use particular SigningServices by setting the ``signing_service_release_overrides`` field on the Repository. For example: + ``signing_service_release_overrides = {"bionic": "/pulp/api/v3/signing-services/433a1f70-c589-4413-a803-c50b842ea9b5/"}`` + 3. Finally, if you have set a ``signing_service`` on a Repository then the other Releases in that Repo will use that Service. .. _verbatim_publishing: diff --git a/pulp_deb/app/migrations/0023_add_default_signing_services.py b/pulp_deb/app/migrations/0023_add_default_signing_services.py new file mode 100644 index 000000000..6df175d4d --- /dev/null +++ b/pulp_deb/app/migrations/0023_add_default_signing_services.py @@ -0,0 +1,36 @@ +# Generated by Django 3.2.16 on 2022-11-28 16:56 + +from django.db import migrations, models +import django.db.models.deletion +import django_lifecycle.mixins +import pulpcore.app.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('deb', '0022_alter_aptdistribution_distribution_ptr_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='aptrepository', + name='signing_service', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='deb.aptreleasesigningservice'), + ), + migrations.CreateModel( + name='AptRepositoryReleaseServiceOverride', + fields=[ + ('pulp_id', models.UUIDField(default=pulpcore.app.models.base.pulp_uuid, editable=False, primary_key=True, serialize=False)), + ('pulp_created', models.DateTimeField(auto_now_add=True)), + ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), + ('release_distribution', models.TextField()), + ('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='signing_service_release_overrides', to='deb.aptrepository')), + ('signing_service', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='deb.aptreleasesigningservice')), + ], + options={ + 'unique_together': {('repository', 'release_distribution')}, + }, + bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), + ), + ] diff --git a/pulp_deb/app/models/__init__.py b/pulp_deb/app/models/__init__.py index 9ce041bba..3f751b36c 100644 --- a/pulp_deb/app/models/__init__.py +++ b/pulp_deb/app/models/__init__.py @@ -8,6 +8,8 @@ Package, ) +from .signing_service import AptReleaseSigningService + from .content.metadata import ( Release, ) @@ -24,6 +26,4 @@ from .remote import AptRemote -from .repository import AptRepository - -from .signing_service import AptReleaseSigningService +from .repository import AptRepository, AptRepositoryReleaseServiceOverride diff --git a/pulp_deb/app/models/repository.py b/pulp_deb/app/models/repository.py index 1e2550449..b214cde35 100644 --- a/pulp_deb/app/models/repository.py +++ b/pulp_deb/app/models/repository.py @@ -1,8 +1,9 @@ -from pulpcore.plugin.models import Repository - +from django.db import models +from pulpcore.plugin.models import BaseModel, Repository from pulpcore.plugin.repo_version_utils import remove_duplicates, validate_version_paths from pulp_deb.app.models import ( + AptReleaseSigningService, AptRemote, GenericContent, InstallerFileIndex, @@ -39,9 +40,27 @@ class AptRepository(Repository): AptRemote, ] + signing_service = models.ForeignKey( + AptReleaseSigningService, on_delete=models.PROTECT, null=True + ) + # Implicit signing_service_release_overrides + class Meta: default_related_name = "%(app_label)s_%(model_name)s" + def release_signing_service(self, release): + """ + Return the Signing Service specified in the overrides if there is one for this release, + else return self.signing_service. + """ + if isinstance(release, Release): + release = release.distribution + try: + override = self.signing_service_release_overrides.get(release_distribution=release) + return override.signing_service + except AptRepositoryReleaseServiceOverride.DoesNotExist: + return self.signing_service + def initialize_new_version(self, new_version): """ Remove old metadata from the repo before performing anything else for the new version. This @@ -75,3 +94,18 @@ def finalize_new_version(self, new_version): if distribution in distributions: raise DuplicateDistributionException(distribution) distributions.append(distribution) + + +class AptRepositoryReleaseServiceOverride(BaseModel): + """ + Override the SigningService that a single Release will use in this AptRepository. + """ + + repository = models.ForeignKey( + AptRepository, on_delete=models.CASCADE, related_name="signing_service_release_overrides" + ) + signing_service = models.ForeignKey(AptReleaseSigningService, on_delete=models.PROTECT) + release_distribution = models.TextField() + + class Meta: + unique_together = (("repository", "release_distribution"),) diff --git a/pulp_deb/app/serializers/repository_serializers.py b/pulp_deb/app/serializers/repository_serializers.py index ab9211fd0..c6266ce03 100644 --- a/pulp_deb/app/serializers/repository_serializers.py +++ b/pulp_deb/app/serializers/repository_serializers.py @@ -1,26 +1,118 @@ from gettext import gettext as _ +from django.db import transaction +from pulpcore.plugin.models import SigningService from pulpcore.plugin.serializers import ( + RelatedField, RepositorySerializer, RepositorySyncURLSerializer, validate_unknown_fields, ) +from pulpcore.plugin.util import get_url -from pulp_deb.app.models import AptRepository +from pulp_deb.app.models import ( + AptRepositoryReleaseServiceOverride, + AptReleaseSigningService, + AptRepository, +) from jsonschema import Draft7Validator from rest_framework import serializers +from rest_framework.exceptions import ValidationError as DRFValidationError from pulp_deb.app.schema import COPY_CONFIG_SCHEMA +class ServiceOverrideField(serializers.DictField): + child = RelatedField( + view_name="signing-services-detail", + queryset=AptReleaseSigningService.objects.all(), + many=False, + required=False, + allow_null=True, + ) + + def to_representation(self, overrides): + return { + # Cast to parent class so get_url can look up resource url. + x.release_distribution: get_url(SigningService(x.signing_service.pk)) + for x in overrides.all() + } + + class AptRepositorySerializer(RepositorySerializer): """ A Serializer for AptRepository. """ + signing_service = RelatedField( + help_text="A reference to an associated signing service. Used if " + "AptPublication.signing_service is not set", + view_name="signing-services-detail", + queryset=AptReleaseSigningService.objects.all(), + many=False, + required=False, + allow_null=True, + ) + signing_service_release_overrides = ServiceOverrideField( + default=dict, + required=False, + help_text=_( + "A dictionary of Release distributions and the Signing Service URLs they should use." + "Example: " + '{"bionic": "/pulp/api/v3/signing-services/433a1f70-c589-4413-a803-c50b842ea9b5/"}' + ), + ) + class Meta: - fields = RepositorySerializer.Meta.fields + fields = RepositorySerializer.Meta.fields + ( + "signing_service", + "signing_service_release_overrides", + ) model = AptRepository + @transaction.atomic + def create(self, validated_data): + """Create an AptRepository, special handling for signing_service_release_overrides.""" + overrides = validated_data.pop("signing_service_release_overrides", -1) + repo = super().create(validated_data) + + try: + self._update_overrides(repo, overrides) + except DRFValidationError as exc: + repo.delete() + raise exc + return repo + + def update(self, instance, validated_data): + """Update an AptRepository, special handling for signing_service_release_overrides.""" + overrides = validated_data.pop("signing_service_release_overrides", -1) + with transaction.atomic(): + self._update_overrides(instance, overrides) + instance = super().update(instance, validated_data) + return instance + + def _update_overrides(self, repo, overrides): + """Update signing_service_release_overrides.""" + if overrides == -1: + # Sentinel value, no updates + return + + current = {x.release_distribution: x for x in repo.signing_service_release_overrides.all()} + # Intentionally only updates items the user specified. + for distro, service in overrides.items(): + if not service and distro in current: # the user wants to delete this override + current[distro].delete() + elif service: + signing_service = AptReleaseSigningService.objects.get(pk=service) + if distro in current: # update + current[distro] = signing_service + current[distro].save() + else: # create + AptRepositoryReleaseServiceOverride( + repository=repo, + signing_service=signing_service, + release_distribution=distro, + ).save() + class AptRepositorySyncURLSerializer(RepositorySyncURLSerializer): """ diff --git a/pulp_deb/app/tasks/publishing.py b/pulp_deb/app/tasks/publishing.py index 68034a54d..7e9a088a3 100644 --- a/pulp_deb/app/tasks/publishing.py +++ b/pulp_deb/app/tasks/publishing.py @@ -21,6 +21,7 @@ from pulp_deb.app.models import ( AptPublication, + AptRepository, Package, PackageReleaseComponent, Release, @@ -101,7 +102,7 @@ def publish(repository_version_pk, simple=False, structured=False, signing_servi publication.simple = simple publication.structured = structured publication.signing_service = signing_service - repository = repo_version.repository + repository = AptRepository.objects.get(pk=repo_version.repository.pk) if simple: codename = "default" @@ -126,6 +127,7 @@ def publish(repository_version_pk, simple=False, structured=False, signing_servi description=repository.description, label=repository.name, version=str(repo_version.number), + signing_service=repository.signing_service, ) for package in Package.objects.filter( @@ -191,6 +193,7 @@ def publish(repository_version_pk, simple=False, structured=False, signing_servi label=repository.name, version=str(repo_version.number), suite=release.suite, + signing_service=repository.release_signing_service(release), ) for prc in PackageReleaseComponent.objects.filter( @@ -279,6 +282,7 @@ def __init__( version, description=None, suite=None, + signing_service=None, ): self.publication = publication self.distribution = distribution @@ -312,7 +316,7 @@ def __init__( self.architectures = architectures self.components = {component: _ComponentHelper(self, component) for component in components} - self.signing_service = publication.signing_service + self.signing_service = publication.signing_service or signing_service def add_metadata(self, metadata): artifact = metadata._artifacts.get() diff --git a/pulp_deb/tests/functional/api/test_publish.py b/pulp_deb/tests/functional/api/test_publish.py index 3652953c6..3a06fef2d 100644 --- a/pulp_deb/tests/functional/api/test_publish.py +++ b/pulp_deb/tests/functional/api/test_publish.py @@ -1,14 +1,22 @@ from random import choice import pytest +import requests from pulp_smash import config -from pulp_smash.pulp3.utils import get_content, get_versions, modify_repo +from pulp_smash.pulp3.utils import ( + download_content_unit, + get_content, + get_versions, + gen_distribution, + modify_repo, +) from pulp_deb.tests.functional.constants import ( DEB_FIXTURE_DISTRIBUTIONS, DEB_GENERIC_CONTENT_NAME, DEB_PACKAGE_NAME, ) +from pulp_deb.tests.functional.utils import deb_distribution_api from pulpcore.client.pulp_deb.exceptions import ApiException from pulpcore.client.pulp_deb import ( @@ -93,6 +101,81 @@ def test_publish_any_repo_version( publication_api.delete(second_publish_href) +@pytest.mark.parallel +@pytest.mark.parametrize( + "set_on,expect_signed", + [ + ("repo", True), + ("release", True), + ("publication", True), + ("nothing", False), + ], +) +def test_publish_signing_services( + deb_remote_factory, + deb_repository_factory, + deb_sync_repository, + gen_object_with_cleanup, + deb_signing_service_factory, + set_on, + expect_signed, + request, +): + """Test whether a signing service preferences are honored. + + The following cases are tested: + + * `Publish where a SigningService is set on the Repo.`_ + * `Publish where a SigningService is set on a Release.`_ + * `Publish where a SigningService is set on a Publication.`_ + """ + publication_api = request.getfixturevalue("apt_publication_api") + repository_api = request.getfixturevalue("apt_repository_api") + cfg = config.get_config() + + # Create a repository with at least two dists + signing_service = deb_signing_service_factory + remote = deb_remote_factory(distributions=DEB_FIXTURE_DISTRIBUTIONS) + distro = DEB_FIXTURE_DISTRIBUTIONS.split()[0] + repo_options = {} + publish_options = {"simple": True, "structured": True} + if set_on == "repo": + repo_options["signing_service"] = signing_service.pulp_href + elif set_on == "release": + repo_options["signing_service_release_overrides"] = {distro: signing_service.pulp_href} + elif set_on == "publication": + publish_options["signing_service"] = signing_service.pulp_href + + repo = deb_repository_factory(**repo_options) + deb_sync_repository(remote, repo) + + # Create a publication supplying the latest `repository_version` + publish_data = DebAptPublication(repository=repo.pulp_href, **publish_options) + publication_href = gen_object_with_cleanup(publication_api, publish_data).pulp_href + + # Create a distribution: + body = gen_distribution() + body["publication"] = publication_href + distribution = gen_object_with_cleanup(deb_distribution_api, body) + + # Check that the expected InRelease file is there: + succeeded = False + try: + download_content_unit(cfg, distribution.to_dict(), "dists/ragnarok/InRelease") + succeeded = True + except requests.exceptions.HTTPError: + pass + + assert expect_signed == succeeded + + # Because the cleanup of the publications happens after we try to delete + # the signing service in the `deb_signing_service_factory` fixture we need to + # delete the publication explicitly here. Otherwise the signing service + # deletion will result in a `django.db.models.deletion.ProtectedError`. + publication_api.delete(publication_href) + repository_api.delete(repo.pulp_href) + + @pytest.fixture def publish_parameters(deb_signing_service_factory): """Fixture for parameters for the publish test."""