From aac67ec87d9730c536226047cc29a600071c75fe Mon Sep 17 00:00:00 2001 From: Tatiana Tereshchenko Date: Mon, 29 Nov 2021 14:46:35 +0100 Subject: [PATCH] Add ManifestSignature model and related serializer/filter/viewset. re #9507 https://pulp.plan.io/issues/9507 --- CHANGES/9507.misc | 1 + .../app/migrations/0021_manifestsignature.py | 34 +++++++++++++ pulp_container/app/models.py | 48 ++++++++++++++++++- pulp_container/app/serializers.py | 33 +++++++++++++ pulp_container/app/viewsets.py | 42 ++++++++++++++++ pulp_container/constants.py | 4 ++ 6 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 CHANGES/9507.misc create mode 100644 pulp_container/app/migrations/0021_manifestsignature.py diff --git a/CHANGES/9507.misc b/CHANGES/9507.misc new file mode 100644 index 000000000..d62995be1 --- /dev/null +++ b/CHANGES/9507.misc @@ -0,0 +1 @@ +Added model, serializer, filter and viewset for image manifest signature. diff --git a/pulp_container/app/migrations/0021_manifestsignature.py b/pulp_container/app/migrations/0021_manifestsignature.py new file mode 100644 index 000000000..7ff7e6e43 --- /dev/null +++ b/pulp_container/app/migrations/0021_manifestsignature.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.9 on 2021-11-29 13:44 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0077_move_remote_url_credentials'), + ('container', '0020_update_push_repo_perms'), + ] + + operations = [ + migrations.CreateModel( + name='ManifestSignature', + fields=[ + ('content_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='container_manifestsignature', serialize=False, to='core.content')), + ('name', models.CharField(db_index=True, max_length=255)), + ('digest', models.CharField(max_length=255)), + ('type', models.CharField(choices=[('atomic', 'atomic')], max_length=255)), + ('key_id', models.CharField(db_index=True, max_length=255)), + ('timestamp', models.PositiveIntegerField()), + ('creator', models.CharField(blank=True, max_length=255)), + ('data', models.BinaryField()), + ('signed_manifest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='signed_manifests', to='container.manifest')), + ], + options={ + 'default_related_name': '%(app_label)s_%(model_name)s', + 'unique_together': {('digest',)}, + }, + bases=('core.content',), + ), + ] diff --git a/pulp_container/app/models.py b/pulp_container/app/models.py index 6c98eb972..c18e261c3 100644 --- a/pulp_container/app/models.py +++ b/pulp_container/app/models.py @@ -35,7 +35,7 @@ from . import downloaders -from pulp_container.constants import MEDIA_TYPE +from pulp_container.constants import MEDIA_TYPE, SIGNATURE_TYPE logger = getLogger(__name__) @@ -200,6 +200,50 @@ class Meta: unique_together = (("name", "tagged_manifest"),) +class ManifestSignature(Content): + """ + A signature for a manifest. + + Fields: + name (models.CharField): A signature name in the 'manifest_digest@random_name' format + digest (models.CharField): A signature sha256 digest. + type (models.CharField): A signature type as specified in signature metadata. Currently + it's only "atomic container signature". + key_id (models.CharField): A key id identified by gpg (last 8 bytes of the fingerprint). + timestamp (models.PositiveIntegerField): A signature timestamp identified by gpg. + creator (models.CharField): A signature creator. + data (models.BinaryField): A signature, base64 encoded. + + Relations: + signed_manifest (models.ForeignKey): A manifest this signature is relevant to. + + """ + + PROTECTED_FROM_RECLAIM = False + + TYPE = "signature" + + SIGNATURE_CHOICES = ((SIGNATURE_TYPE.ATOMIC_SHORT, SIGNATURE_TYPE.ATOMIC_SHORT),) + + name = models.CharField(max_length=255, db_index=True) + digest = models.CharField(max_length=255) + type = models.CharField(max_length=255, choices=SIGNATURE_CHOICES) + key_id = models.CharField(max_length=255, db_index=True) + timestamp = models.PositiveIntegerField() + creator = models.CharField(max_length=255, blank=True) + data = models.BinaryField() + + signed_manifest = models.ForeignKey( + Manifest, null=False, related_name="signed_manifests", on_delete=models.CASCADE + ) + # TODO: Maybe there should be an optional field with a FK to a signing_service for the cases + # when Pulp creates a signature. + + class Meta: + default_related_name = "%(app_label)s_%(model_name)s" + unique_together = (("digest",),) + + class ContainerNamespace(BaseModel, AutoAddObjPermsMixin): """ Namespace for the container registry. @@ -370,7 +414,7 @@ class ContainerRepository( """ TYPE = "container" - CONTENT_TYPES = [Blob, Manifest, Tag] + CONTENT_TYPES = [Blob, Manifest, Tag, ManifestSignature] REMOTE_TYPES = [ContainerRemote] PUSH_ENABLED = False ACCESS_POLICY_VIEWSET_NAME = "repositories/container/container" diff --git a/pulp_container/app/serializers.py b/pulp_container/app/serializers.py index dc1354530..7c87f2556 100644 --- a/pulp_container/app/serializers.py +++ b/pulp_container/app/serializers.py @@ -125,6 +125,39 @@ class Meta: model = models.Blob +class ManifestSignatureSerializer(NoArtifactContentSerializer): + """ + Serializer for image manifest signatures. + """ + + name = serializers.CharField( + help_text="Signature name in the format of `digest_algo:manifest_digest@random_32_chars`" + ) + digest = serializers.CharField(help_text="sha256 digest of the signature blob") + type = serializers.CharField(help_text="Container signature type, e.g. 'atomic'") + key_id = serializers.CharField(help_text="Signing key ID") + timestamp = serializers.IntegerField(help_text="Timestamp of a signature") + creator = serializers.CharField(help_text="Signature creator") + signed_manifest = DetailRelatedField( + many=False, + help_text="Manifest that is signed", + view_name="container-manifests-detail", + queryset=models.Manifest.objects.all(), + ) + + class Meta: + fields = NoArtifactContentSerializer.Meta.fields + ( + "name", + "digest", + "type", + "key_id", + "timestamp", + "creator", + "signed_manifest", + ) + model = models.ManifestSignature + + class RegistryPathField(serializers.CharField): """ Serializer Field for the registry_path field of the ContainerDistribution. diff --git a/pulp_container/app/viewsets.py b/pulp_container/app/viewsets.py index 70073a5a2..39073413b 100644 --- a/pulp_container/app/viewsets.py +++ b/pulp_container/app/viewsets.py @@ -100,6 +100,22 @@ class Meta: } +class ManifestSignatureFilter(ContentFilter): + """ + FilterSet for image signatures. + """ + + manifest = CharInFilter(field_name="signed_manifest__digest", lookup_expr="in") + + class Meta: + model = models.ManifestSignature + fields = { + "name": NAME_FILTER_OPTIONS, + "digest": ["exact", "in"], + "key_id": ["exact", "in"], + } + + class ContainerDistributionFilter(DistributionFilter): """ FilterSet for ContainerDistributions @@ -294,6 +310,32 @@ class BlobViewSet(ContainerContentQuerySetMixin, ReadOnlyContentViewSet): } +class ManifestSignatureViewSet(ContainerContentQuerySetMixin, ReadOnlyContentViewSet): + """ + ViewSet for image signatures. + """ + + endpoint_name = "signatures" + queryset = models.ManifestSignature.objects.all() + serializer_class = serializers.ManifestSignatureSerializer + filterset_class = ManifestSignatureFilter + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + ], + } + + class ContainerRemoteViewSet(RemoteViewSet): """ Container remotes represent an external repository that implements the Container diff --git a/pulp_container/constants.py b/pulp_container/constants.py index 7da6b4f96..18c6a063b 100644 --- a/pulp_container/constants.py +++ b/pulp_container/constants.py @@ -17,3 +17,7 @@ ) EMPTY_BLOB = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" BLOB_CONTENT_TYPE = "application/octet-stream" +SIGNATURE_TYPE = SimpleNamespace( + ATOMIC_FULL="atomic container signature", # full version is present in the signed document + ATOMIC_SHORT="atomic", # short version is used in the JSON produced by API extension +)