Skip to content

Improve OpenPGP keyring test coverage - #7947

Draft
dralley wants to merge 7 commits into
pulp:mainfrom
dralley:openpgp
Draft

Improve OpenPGP keyring test coverage#7947
dralley wants to merge 7 commits into
pulp:mainfrom
dralley:openpgp

Conversation

@dralley

@dralley dralley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor
  • v4/v6 key uploads (RSA, Ed25519)
  • PQC keys (ML-DSA-65, ML-DSA-87)
  • content listing/filtering
  • serialization round-trips via distribution
  • signature verification using keys served from the keyring
  • key revocation
  • idempotent uploads
  • private key rejection

Also fix three bugs:

  • Typo: distribution permission said "gem" instead of "openpgp"
  • content_handler_list_directory crashed when no repository version exists
  • represent() querysets were unordered, producing nondeterministic output

Assisted-By: Claude Opus 4.6

📜 Checklist

  • Commits are cleanly separated with meaningful messages (simple features and bug fixes should be squashed to one commit)
  • A changelog entry or entries has been added for any significant changes
  • Follows the Pulp policy on AI Usage
  • (For new features) - User documentation and test coverage has been added

See: Pull Request Walkthrough

data = self.packet()
# note: because these queries aren't ordered, the result may be nondetermininistic
for signature in self.openpgp_signatures.filter(**content_filter):
for signature in self.openpgp_signatures.filter(**content_filter).order_by("pk"):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we try to order by something different than PK? Problem is we don't really have access to much else.

"permissions": [
(
"manage_roles_openpgpdistribution",
"Can manage roles on openpgp distributions",

@dralley dralley Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So irritating that just fixing a typo requires a migration. I will probably try to see if we can fold this one into another one, should another one come along soon.

)
assert keys.count == 1
key = keys.results[0]
assert len(key.fingerprint) == 64

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is implicit in the regex on the next line.

Also this looks kind of repetitive maybe you could @pytest.mark.parametrize these upload tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already ahead of you

result = verify(bytes=test_data, store=lambda key_ids: served_certs, signature=sig)
assert result.valid_sigs

def test_verify_signature_multiple_keys_in_keyring(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could perhaps be consolidated with the previous one, but parameterize it is a bit trickier than the others.

@dralley

dralley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@mdellweg Sorry for requesting review prematurely, I wanted to try to get some early feedback before EOD. But yes, the initial state was very rough, and now it's much more cleaned up.

@dralley

dralley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Also I expect some of these tests to fail, since PySequoia isn't quite PQC ready yet.

edit: also I found a bug. Either way we have to wait for the next release.

@dralley

dralley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@mdellweg Do we need an access policy on the OpenPGPDistributionViewSet? Or is the "default" one fine? I'm not sure if the commented out DEFAULT_ACCESS_POLICY is meant to be informational or as a forgotten TODO placeholder

@dralley
dralley force-pushed the openpgp branch 2 times, most recently from 426c99e to 34c797c Compare August 7, 2026 17:44
@dralley

dralley commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@mdellweg So, here's one issue.

Some of the failures are caused because get_viewset_for_model() fails, and that happens because OpenPGPKeyringVersionViewSet is defined in the same core app as RepositoryVersionViewSet. import_viewsets() runs once per app so the detection gets messy when you have subclasses in a single app. Generally speaking, detail viewsets are defined in other apps (even pulp_file is a separate app, despite being embedded in pulpcore).

So we could either migrate it to a separate app, or change the implementation of import_viewsets(), or do what ListRepositoryVersionViewSet does, don't subclass NamedModelViewSet, and register the URL manually.

dralley added 5 commits August 7, 2026 17:31
- v4/v6 key uploads (RSA, Ed25519)
- PQC keys (ML-DSA-65, ML-DSA-87)
- content listing/filtering
- serialization round-trips via distribution
- signature verification using keys served from the keyring
- key revocation
- idempotent uploads
- private key rejection

Also fix three bugs:
- Typo: distribution permission said "gem" instead of "openpgp"
- content_handler_list_directory crashed when no repository version exists
- represent() querysets were unordered, producing nondeterministic output

Assisted-By: Claude Opus 4.6
A public key packet needs to be before any other packets
And store fingerprints uppercase consistently in the DB.
Add a viewset for repository versions.

Assisted-By: Claude Opus 4.6
The `expired` property used short-circuit evaluation
(`self.expiration_time and ...`) which returns None when
expiration_time is None. The serializer declares `expired` as a
non-nullable BooleanField, so None causes a pydantic ValidationError
in the generated client bindings.

Wrap the expression in bool() so it always returns False when
expiration_time is unset.

Assisted-By: Claude Opus 4.6
Comment thread pulpcore/app/util.py
# Multiple viewsets for the same model (e.g. RepositoryVersionViewSet
# and OpenPGPKeyringVersionViewSet both use RepositoryVersion).
# Prefer the base class — subclass viewsets exist for URL routing
# and custom access policies, not for model-to-viewset reverse lookups.

@dralley dralley Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is theoretically correct (I think), but it's a huge hack that Claude came up with, and I'd prefer to just move the openpgp types to a new app. There's also conflicts with implementing import/export for types in pulpcore.

If we were to actually move forwards with this I'd want to review this very very heavily. I have not really done so yet, I just want to see the impact it has on CI.

When an app registers multiple NamedModelViewSet subclasses for the
same model (e.g. RepositoryVersionViewSet and
OpenPGPKeyringVersionViewSet both use RepositoryVersion), the lookup
previously skipped the model entirely, causing LookupError in the
import/export system.

Resolve the ambiguity by preferring the base class in the inheritance
hierarchy. Subclass viewsets still participate in URL routing and
provide their own access policies — they just aren't the canonical
answer for model-to-viewset reverse lookups.

Assisted-By: Claude Opus 4.6
The field exists on the model but was missing from the serializer's
fields tuple, causing an AttributeError in the client bindings when
tests accessed it.

Assisted-By: Claude Opus 4.6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants