pulp-container 2.29.2__tar.gz → 2.29.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. {pulp_container-2.29.2 → pulp_container-2.29.3}/CHANGES.md +14 -0
  2. {pulp_container-2.29.2 → pulp_container-2.29.3}/PKG-INFO +1 -1
  3. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/__init__.py +1 -1
  4. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/downloaders.py +35 -14
  5. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/registry_api.py +44 -7
  6. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/viewsets.py +12 -0
  7. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_pull_content.py +61 -1
  8. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_pull_through_cache.py +48 -22
  9. pulp_container-2.29.3/pulp_container/tests/unit/test_downloaders.py +33 -0
  10. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container.egg-info/PKG-INFO +1 -1
  11. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container.egg-info/SOURCES.txt +1 -0
  12. {pulp_container-2.29.2 → pulp_container-2.29.3}/pyproject.toml +2 -2
  13. {pulp_container-2.29.2 → pulp_container-2.29.3}/COMMITMENT +0 -0
  14. {pulp_container-2.29.2 → pulp_container-2.29.3}/COPYRIGHT +0 -0
  15. {pulp_container-2.29.2 → pulp_container-2.29.3}/LICENSE +0 -0
  16. {pulp_container-2.29.2 → pulp_container-2.29.3}/MANIFEST.in +0 -0
  17. {pulp_container-2.29.2 → pulp_container-2.29.3}/README.rst +0 -0
  18. {pulp_container-2.29.2 → pulp_container-2.29.3}/functest_requirements.txt +0 -0
  19. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/__init__.py +0 -0
  20. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/access_policy.py +0 -0
  21. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/authorization.py +0 -0
  22. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/cache.py +0 -0
  23. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/checks.py +0 -0
  24. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/content.py +0 -0
  25. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/dynaconf_hooks.py +0 -0
  26. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/exceptions.py +0 -0
  27. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/fields.py +0 -0
  28. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/global_access_conditions.py +0 -0
  29. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/json_schemas.py +0 -0
  30. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/management/__init__.py +0 -0
  31. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/management/commands/__init__.py +0 -0
  32. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/management/commands/container-handle-image-data.py +0 -0
  33. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/management/commands/container-repair-media-type.py +0 -0
  34. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/management/commands/container-repair-pull-through-distributions.py +0 -0
  35. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0001_initial.py +0 -0
  36. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0001_squashed_0032_upload_artifact.py +0 -0
  37. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0002_containerrepository.py +0 -0
  38. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0003_oci_mediatype.py +0 -0
  39. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0004_upload.py +0 -0
  40. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0005_contentredirectcontentguard.py +0 -0
  41. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0006_containerpushrepository.py +0 -0
  42. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0007_clear_tags_artifacts_refs.py +0 -0
  43. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0008_include_exclude_tags.py +0 -0
  44. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0009_container_namespace.py +0 -0
  45. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0010_remove_uploadchunk.py +0 -0
  46. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0011_add_container_repository_permissions.py +0 -0
  47. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0012_add_container_namespace_permissions.py +0 -0
  48. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0013_add_pull_push_permissions.py +0 -0
  49. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0014_containerdistribution_private.py +0 -0
  50. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0015_manage_tags_push_repo.py +0 -0
  51. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0016_add_delete_versions_permission.py +0 -0
  52. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0017_add_granular_perms.py +0 -0
  53. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0018_containerdistribution_description.py +0 -0
  54. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0019_DATA_distribution_model_swap.py +0 -0
  55. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0020_update_push_repo_perms.py +0 -0
  56. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0021_data_move_redirect_content_guard_to_core.py +0 -0
  57. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0022_delete_contentredirectcontentguard.py +0 -0
  58. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0023_manifestsignature.py +0 -0
  59. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0024_containerremote_sigstore.py +0 -0
  60. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0025_signature_stored_in_textfield.py +0 -0
  61. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0026_manifest_signing_service.py +0 -0
  62. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0027_data_translate_perms_to_roles.py +0 -0
  63. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0028_add_role_manage_permissions.py +0 -0
  64. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0029_remove_blob_media_type.py +0 -0
  65. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0030_enforce_tagged_manifest_reference.py +0 -0
  66. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0031_replace_charf_with_textf.py +0 -0
  67. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0032_upload_artifact.py +0 -0
  68. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0033_raise_warning_for_repair.py +0 -0
  69. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0034_translate_signed_schema.py +0 -0
  70. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0035_alter_blob_content_ptr_and_more.py +0 -0
  71. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0036_containerpushrepository_pending_blobs_manifests.py +0 -0
  72. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0037_create_pull_through_cache_models.py +0 -0
  73. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0038_add_manifest_metadata_fields.py +0 -0
  74. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0039_manifest_data.py +0 -0
  75. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0040_add_remote_repo_filter.py +0 -0
  76. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0041_add_pull_through_pull_permissions.py +0 -0
  77. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0042_add_manifest_nature_field.py +0 -0
  78. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0043_add_os_arch_image_size_manifest_fields.py +0 -0
  79. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0044_add_domain.py +0 -0
  80. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0045_alter_manifest_compressed_image_size.py +0 -0
  81. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0046_alter_manifest_listed_manifests.py +0 -0
  82. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0047_containernamespace_pulp_labels.py +0 -0
  83. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0048_containerremote_includes_excludes.py +0 -0
  84. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0049_manifestsignature_fingerprint.py +0 -0
  85. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/0050_alter_containernamespace_options.py +0 -0
  86. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/migrations/__init__.py +0 -0
  87. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/modelresource.py +0 -0
  88. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/models.py +0 -0
  89. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/pull_through.py +0 -0
  90. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/redirects.py +0 -0
  91. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/registry.py +0 -0
  92. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/replica.py +0 -0
  93. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/serializers.py +0 -0
  94. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/settings.py +0 -0
  95. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/__init__.py +0 -0
  96. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/builder.py +0 -0
  97. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/download_image_data.py +0 -0
  98. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/migrate_push_repository.py +0 -0
  99. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/recursive_add.py +0 -0
  100. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/recursive_remove.py +0 -0
  101. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/sign.py +0 -0
  102. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/sync_stages.py +0 -0
  103. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/synchronize.py +0 -0
  104. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/tag.py +0 -0
  105. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/tasks/untag.py +0 -0
  106. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/token_verification.py +0 -0
  107. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/urls.py +0 -0
  108. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/utils.py +0 -0
  109. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/webserver_snippets/__init__.py +0 -0
  110. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/webserver_snippets/apache.conf +0 -0
  111. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/app/webserver_snippets/nginx.conf +0 -0
  112. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/constants.py +0 -0
  113. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/__init__.py +0 -0
  114. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/__init__.py +0 -0
  115. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/__init__.py +0 -0
  116. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_build_images.py +0 -0
  117. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_cancel_blob_upload.py +0 -0
  118. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_content_cache.py +0 -0
  119. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_crud_distributions.py +0 -0
  120. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_crud_remotes.py +0 -0
  121. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_delete_blob.py +0 -0
  122. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_delete_manifest.py +0 -0
  123. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_domains.py +0 -0
  124. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_flatpak.py +0 -0
  125. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_migrate_push_repository.py +0 -0
  126. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_pulpimportexport.py +0 -0
  127. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_push_content.py +0 -0
  128. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_push_signatures.py +0 -0
  129. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_rbac_push_repositories.py +0 -0
  130. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_rbac_remotes.py +0 -0
  131. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_rbac_repo_content.py +0 -0
  132. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_rbac_repo_versions.py +0 -0
  133. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_rbac_sync_repositories.py +0 -0
  134. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_recursive_add.py +0 -0
  135. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_recursive_remove.py +0 -0
  136. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_remote_filter_pull_through.py +0 -0
  137. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_repositories_list.py +0 -0
  138. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_sign_manifests.py +0 -0
  139. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_sync.py +0 -0
  140. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_sync_signatures.py +0 -0
  141. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_tagging_images.py +0 -0
  142. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/api/test_token_authentication.py +0 -0
  143. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/conftest.py +0 -0
  144. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/constants.py +0 -0
  145. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/functional/utils.py +0 -0
  146. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/__init__.py +0 -0
  147. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/fixtures/ryuk_0.13.0_manifest_list.json +0 -0
  148. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/test_json_schemas.py +0 -0
  149. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/test_models.py +0 -0
  150. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/test_pull_through_repair.py +0 -0
  151. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/test_serializers.py +0 -0
  152. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container/tests/unit/test_sync_stages.py +0 -0
  153. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container.egg-info/dependency_links.txt +0 -0
  154. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container.egg-info/entry_points.txt +0 -0
  155. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container.egg-info/requires.txt +0 -0
  156. {pulp_container-2.29.2 → pulp_container-2.29.3}/pulp_container.egg-info/top_level.txt +0 -0
  157. {pulp_container-2.29.2 → pulp_container-2.29.3}/setup.cfg +0 -0
  158. {pulp_container-2.29.2 → pulp_container-2.29.3}/test_requirements.txt +0 -0
  159. {pulp_container-2.29.2 → pulp_container-2.29.3}/unittest_requirements.txt +0 -0
@@ -8,6 +8,20 @@
8
8
 
9
9
  [//]: # (towncrier release notes start)
10
10
 
11
+ ## 2.29.3 (2026-10-28) {: #2.29.3 }
12
+
13
+ #### Bugfixes {: #2.29.3-bugfix }
14
+
15
+ - Allow anonymous pull-through for public distributions
16
+ [#1976](https://github.com/pulp/pulp_container/issues/1976)
17
+ - Container blob downloads now respect each Domain's object storage redirect setting.
18
+ - Fixed pull-through signature lookups failing with a 500 when the upstream registry (e.g. gcr.io) sends unquoted values in its bearer auth challenge.
19
+ - Fixed the registry Authorization header leaking into a shared module-level headers dict, which could send one remote's credentials to another registry.
20
+ - Retried GHCR requests that fail with 403 using a bearer token, and fell back to GET when a registry (e.g. ECR Public) rejects HEAD on blob endpoints.
21
+ - Treat an explicitly empty Accept header like a missing header when retrieving registry blobs.
22
+
23
+ ---
24
+
11
25
  ## 2.29.2 (2026-10-08) {: #2.29.2 }
12
26
 
13
27
  #### Bugfixes {: #2.29.2-bugfix }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-container
3
- Version: 2.29.2
3
+ Version: 2.29.3
4
4
  Summary: Container plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -24,7 +24,7 @@ class PulpContainerPluginAppConfig(PulpPluginAppConfig):
24
24
 
25
25
  name = "pulp_container.app"
26
26
  label = "container"
27
- version = "2.29.2"
27
+ version = "2.29.3"
28
28
  python_package_name = "pulp-container"
29
29
  domain_compatible = True
30
30
 
@@ -33,7 +33,7 @@ class RegistryAuthHttpDownloader(HttpDownloader):
33
33
  self.factory = kwargs.pop("factory")
34
34
  super().__init__(*args, **kwargs)
35
35
 
36
- async def _run(self, handle_401=True, extra_data=None):
36
+ async def _run(self, token_refresh_retries=2, extra_data=None):
37
37
  """
38
38
  Download, validate, and compute digests on the `url`. This is a coroutine.
39
39
 
@@ -45,13 +45,13 @@ class RegistryAuthHttpDownloader(HttpDownloader):
45
45
  :meth:`~pulpcore.plugin.download.BaseDownloader._run`.
46
46
 
47
47
  Args:
48
- handle_401(bool): If true, catch 401, request a new token and retry.
48
+ token_refresh_retries(int): Number of times to retry the request
49
49
 
50
50
  """
51
51
  headers = {}
52
52
  repo_name = None
53
53
  if extra_data is not None:
54
- headers = extra_data.get("headers", headers)
54
+ headers = dict(extra_data.get("headers", headers))
55
55
  repo_name = extra_data.get("repo_name", None)
56
56
  http_method = extra_data.get("http_method", "get") if extra_data is not None else "get"
57
57
  this_token = self.factory.registry_auth["bearer"]
@@ -73,7 +73,7 @@ class RegistryAuthHttpDownloader(HttpDownloader):
73
73
  except ClientResponseError as e:
74
74
  response_auth_header = response.headers.get("www-authenticate")
75
75
  # Need to retry request
76
- if handle_401 and e.status == 401 and response_auth_header is not None:
76
+ if token_refresh_retries and e.status == 401 and response_auth_header is not None:
77
77
  # check if bearer or basic
78
78
  if "bearer" in response_auth_header.lower():
79
79
  # Token has not been updated during request
@@ -83,12 +83,22 @@ class RegistryAuthHttpDownloader(HttpDownloader):
83
83
  ):
84
84
  self.factory.registry_auth["bearer"] = None
85
85
  await self.update_token(response_auth_header, this_token, repo_name)
86
- return await self._run(handle_401=False, extra_data=extra_data)
86
+ return await self._run(
87
+ token_refresh_retries=token_refresh_retries - 1, extra_data=extra_data
88
+ )
87
89
  elif "basic" in response_auth_header.lower():
88
90
  if self.remote.username:
89
91
  basic = aiohttp.BasicAuth(self.remote.username, self.remote.password)
90
92
  self.factory.registry_auth["basic"] = basic.encode()
91
- return await self._run(handle_401=False, extra_data=extra_data)
93
+ return await self._run(
94
+ token_refresh_retries=token_refresh_retries - 1, extra_data=extra_data
95
+ )
96
+ elif token_refresh_retries and e.status == 403 and this_token is not None:
97
+ # handle 403 with bearer token, this is a special case for ghcr registry
98
+ self.factory.registry_auth["bearer"] = None
99
+ return await self._run(
100
+ token_refresh_retries=token_refresh_retries - 1, extra_data=extra_data
101
+ )
92
102
  else:
93
103
  raise
94
104
 
@@ -115,14 +125,7 @@ class RegistryAuthHttpDownloader(HttpDownloader):
115
125
  ):
116
126
  return
117
127
  log.info("Updating bearer token")
118
- bearer_info_string = response_auth_header[len("Bearer ") :]
119
- bearer_info_list = re.split(",(?=[^=,]+=)", bearer_info_string)
120
-
121
- # The remaining string consists of comma seperated key=value pairs
122
- auth_query_dict = {}
123
- for key, value in (item.split("=") for item in bearer_info_list):
124
- # The value is a string within a string, ex: '"value"'
125
- auth_query_dict[key] = json.loads(value)
128
+ auth_query_dict = self.parse_auth_challenge(response_auth_header)
126
129
  try:
127
130
  token_base_url = auth_query_dict.pop("realm")
128
131
  except KeyError:
@@ -156,6 +159,24 @@ class RegistryAuthHttpDownloader(HttpDownloader):
156
159
  token = token_js.get("token") or token_js.get("access_token")
157
160
  self.factory.registry_auth["bearer"] = token
158
161
 
162
+ @staticmethod
163
+ def parse_auth_challenge(response_auth_header):
164
+ """
165
+ Parse the parameters of a Bearer auth challenge into a dictionary.
166
+
167
+ Values may be quoted strings or plain tokens (RFC 7235), e.g. gcr.io responds with
168
+ 'Bearer realm="https://gcr.io/v2/token",service=gcr.io'.
169
+ """
170
+ bearer_info_string = response_auth_header[len("Bearer ") :]
171
+ auth_query_dict = {}
172
+ for match in re.finditer(r'([^=,\s]+)=(?:"((?:[^"\\]|\\.)*)"|([^,]*))', bearer_info_string):
173
+ key, quoted, plain = match.groups()
174
+ if quoted is not None:
175
+ auth_query_dict[key] = re.sub(r"\\(.)", r"\1", quoted)
176
+ else:
177
+ auth_query_dict[key] = plain.strip()
178
+ return auth_query_dict
179
+
159
180
  @staticmethod
160
181
  def auth_header(token, basic_auth):
161
182
  """
@@ -32,6 +32,7 @@ from rest_framework.exceptions import (
32
32
  Throttled,
33
33
  )
34
34
  from rest_framework.generics import ListAPIView
35
+ from rest_framework.negotiation import DefaultContentNegotiation
35
36
  from rest_framework.pagination import BasePagination
36
37
  from rest_framework.permissions import IsAuthenticated
37
38
  from rest_framework.renderers import BaseRenderer, JSONRenderer
@@ -142,6 +143,15 @@ class ContentRenderer(BaseRenderer):
142
143
  return data
143
144
 
144
145
 
146
+ class BlobContentNegotiation(DefaultContentNegotiation):
147
+ """Treat an explicitly empty Accept header like a missing header for blobs."""
148
+
149
+ def get_accept_list(self, request):
150
+ if request.headers.get("Accept") == "":
151
+ return ["*/*"]
152
+ return super().get_accept_list(request)
153
+
154
+
145
155
  class ManifestResponse(Response):
146
156
  """
147
157
  An HTTP response class for returning Manifets.
@@ -371,7 +381,11 @@ class ContainerRegistryApiMixin:
371
381
  def get_pull_through_drv(self, path):
372
382
  domain = get_domain()
373
383
  pull_through_cache_distribution = get_pull_through_distribution(path, domain)
374
- if not pull_through_cache_distribution or not self.request.user.is_authenticated:
384
+ # allow anonymous pull-through access for public distributions
385
+ if not pull_through_cache_distribution:
386
+ raise RepositoryNotFound(name=path)
387
+ # user must be authenticated when pull-through is private
388
+ if pull_through_cache_distribution.private and not self.request.user.is_authenticated:
375
389
  raise RepositoryNotFound(name=path)
376
390
 
377
391
  pull_through_path = get_pull_through_distribution_path(pull_through_cache_distribution)
@@ -1122,7 +1136,7 @@ class RedirectsMixin:
1122
1136
  domain = get_domain()
1123
1137
  if (
1124
1138
  domain.storage_class == "pulpcore.app.models.storage.FileSystem"
1125
- or not settings.REDIRECT_TO_OBJECT_STORAGE
1139
+ or not domain.redirect_to_object_storage
1126
1140
  ):
1127
1141
  self.redirects_class = FileStorageRedirects
1128
1142
  elif domain.storage_class == "storages.backends.s3boto3.S3Boto3Storage":
@@ -1139,6 +1153,7 @@ class Blobs(RedirectsMixin, ContainerRegistryApiMixin, ViewSet):
1139
1153
  """
1140
1154
 
1141
1155
  renderer_classes = [ContentRenderer]
1156
+ content_negotiation_class = BlobContentNegotiation
1142
1157
 
1143
1158
  def head(self, request, path, pk=None):
1144
1159
  """
@@ -1204,10 +1219,25 @@ class Blobs(RedirectsMixin, ContainerRegistryApiMixin, ViewSet):
1204
1219
  relative_url = "/v2/{name}/blobs/{pk}".format(name=remote.namespaced_upstream_name, pk=pk)
1205
1220
  blob_url = urljoin(remote.url, relative_url)
1206
1221
  downloader = remote.get_downloader(url=blob_url)
1222
+ used_get_fallback = False
1207
1223
  try:
1208
- response = downloader.fetch(
1209
- extra_data={"headers": V2_ACCEPT_HEADERS, "http_method": "head"}
1210
- )
1224
+ try:
1225
+ response = downloader.fetch(
1226
+ extra_data={"headers": V2_ACCEPT_HEADERS, "http_method": "head"}
1227
+ )
1228
+ except ClientResponseError as response_error:
1229
+ if response_error.status in (401, 405):
1230
+ # Some registries (e.g. ECR Public) reject HEAD on blob endpoints
1231
+ # regardless of authentication; verify existence via GET instead
1232
+ used_get_fallback = True
1233
+ response = downloader.fetch(
1234
+ extra_data={
1235
+ "headers": {**V2_ACCEPT_HEADERS, "Range": "bytes=0-0"},
1236
+ "http_method": "get",
1237
+ }
1238
+ )
1239
+ else:
1240
+ raise
1211
1241
  except ClientResponseError as response_error:
1212
1242
  if response_error.status == 429:
1213
1243
  # the client could request the blob outside the docker hub pull limit;
@@ -1221,7 +1251,10 @@ class Blobs(RedirectsMixin, ContainerRegistryApiMixin, ViewSet):
1221
1251
  # The remote server is not available at the moment
1222
1252
  raise GatewayTimeout()
1223
1253
  else:
1224
- if response.headers.get("docker-content-digest") != pk:
1254
+ # Registries that reject HEAD answer the ranged GET with a redirect that
1255
+ # carries no docker-content-digest; the digest is verified later when the
1256
+ # artifact is actually downloaded.
1257
+ if not used_get_fallback and response.headers.get("docker-content-digest") != pk:
1225
1258
  raise BlobNotFound(digest=pk)
1226
1259
  return blob_url
1227
1260
 
@@ -1772,7 +1805,11 @@ class Signatures(ContainerRegistryApiMixin, ViewSet):
1772
1805
  response_headers = {}
1773
1806
  try:
1774
1807
  ping_response = ping_downloader.fetch(
1775
- extra_data={"headers": V2_ACCEPT_HEADERS, "http_method": "get"}
1808
+ extra_data={
1809
+ "headers": V2_ACCEPT_HEADERS,
1810
+ "http_method": "get",
1811
+ "repo_name": remote.namespaced_upstream_name,
1812
+ }
1776
1813
  )
1777
1814
  response_headers = ping_response.headers
1778
1815
  except ClientResponseError as e:
@@ -1396,6 +1396,12 @@ class ContainerDistributionViewSet(DistributionViewSet, RolesMixin):
1396
1396
  "has_model_or_domain_or_obj_perms:container.manage_roles_containerdistribution"
1397
1397
  ],
1398
1398
  },
1399
+ {
1400
+ "action": ["pull_new_content"],
1401
+ "principal": "*",
1402
+ "effect": "allow",
1403
+ "condition_expression": "not is_private",
1404
+ },
1399
1405
  {
1400
1406
  "action": ["pull_new_content"],
1401
1407
  "principal": "authenticated",
@@ -1562,6 +1568,12 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin):
1562
1568
  "has_model_or_domain_or_obj_perms:container.manage_roles_containerpullthroughdistribution" # noqa
1563
1569
  ],
1564
1570
  },
1571
+ {
1572
+ "action": ["pull_new_distribution"],
1573
+ "principal": "*",
1574
+ "effect": "allow",
1575
+ "condition_expression": "not is_private",
1576
+ },
1565
1577
  {
1566
1578
  "action": ["pull_new_distribution"],
1567
1579
  "principal": "authenticated",
@@ -118,7 +118,7 @@ class TestPullContent:
118
118
  )
119
119
  assert content_response.content == digest_response.content
120
120
 
121
- def test_create_empty_blob_on_the_fly(self, bindings_cfg, full_path, setup):
121
+ def test_create_empty_blob_on_the_fly(self, bindings_cfg, full_path, setup, local_registry):
122
122
  """
123
123
  Test if empty blob getscreated and served on the fly.
124
124
  """
@@ -135,6 +135,66 @@ class TestPullContent:
135
135
  header_digest = content_response.headers["docker-content-digest"].split(":")[1]
136
136
  assert digest == header_digest
137
137
 
138
+ head, _ = local_registry.get_response("HEAD", blob_path)
139
+ assert head.status_code == 200
140
+ assert head.headers["Docker-Content-Digest"] == EMPTY_BLOB
141
+ assert int(head.headers["Content-Length"]) == len(content_response.content)
142
+ assert head.content == b""
143
+
144
+ @pytest.mark.parametrize("blob_type", ["config", "layer"])
145
+ @pytest.mark.parametrize("method", ["GET", "HEAD"])
146
+ @pytest.mark.parametrize("accept", [None, "", "*/*"])
147
+ def test_blob_accept_header(
148
+ self, blob_type, method, accept, bindings_cfg, full_path, setup, local_registry
149
+ ):
150
+ """Blob GET and HEAD redirect successfully for any supported Accept header."""
151
+ _, distribution, _ = setup
152
+ path = full_path(distribution)
153
+ tag = PULP_HELLO_WORLD_LINUX_TAG.lstrip(":")
154
+ manifest_headers = {
155
+ "Accept": f"{MEDIA_TYPE.MANIFEST_V2}, {MEDIA_TYPE.MANIFEST_OCI}, "
156
+ f"{MEDIA_TYPE.MANIFEST_LIST}, {MEDIA_TYPE.INDEX_OCI}"
157
+ }
158
+ manifest, _ = local_registry.get_response(
159
+ "GET", f"/v2/{path}/manifests/{tag}", headers=manifest_headers
160
+ )
161
+ manifest.raise_for_status()
162
+ manifest_data = manifest.json()
163
+ while "manifests" in manifest_data:
164
+ child_digest = manifest_data["manifests"][0]["digest"]
165
+ manifest, _ = local_registry.get_response(
166
+ "GET", f"/v2/{path}/manifests/{child_digest}", headers=manifest_headers
167
+ )
168
+ manifest.raise_for_status()
169
+ manifest_data = manifest.json()
170
+ descriptor = (
171
+ manifest_data["config"] if blob_type == "config" else manifest_data["layers"][0]
172
+ )
173
+ digest = descriptor["digest"]
174
+ blob_path = f"/v2/{path}/blobs/{digest}"
175
+ headers = {"Accept": accept}
176
+
177
+ response, auth = local_registry.get_response(
178
+ method, blob_path, headers=headers, allow_redirects=False
179
+ )
180
+ assert response.status_code == 302
181
+ assert response.headers["Location"]
182
+
183
+ if method == "HEAD":
184
+ head = requests.head(
185
+ urljoin(bindings_cfg.host, blob_path),
186
+ auth=auth,
187
+ headers=headers,
188
+ allow_redirects=True,
189
+ )
190
+ assert head.status_code == 200
191
+ assert int(head.headers["Content-Length"]) == descriptor["size"]
192
+ assert head.content == b""
193
+
194
+ download = requests.get(urljoin(bindings_cfg.host, blob_path), auth=auth, headers=headers)
195
+ download.raise_for_status()
196
+ assert f"sha256:{hashlib.sha256(download.content).hexdigest()}" == digest
197
+
138
198
  def test_pull_image_from_repository(self, local_registry, registry_client, full_path, setup):
139
199
  """Verify that a client can pull the image from Pulp.
140
200
 
@@ -60,6 +60,9 @@ def pull_and_verify(
60
60
  monitor_task,
61
61
  ):
62
62
  def _pull_and_verify(images, pull_through_distribution):
63
+ if pull_through_distribution.private and settings.TOKEN_AUTH_DISABLED:
64
+ pytest.skip("RBAC cannot be tested when token authentication is disabled")
65
+
63
66
  tags_to_verify = []
64
67
  pull_through_path = pull_through_distribution.pulp_labels[PULL_THROUGH_DISTRIBUTION_LABEL]
65
68
  for version, image_path in enumerate(images, start=1):
@@ -67,12 +70,14 @@ def pull_and_verify(
67
70
  local_image_path = f"{pull_through_path}/{image_path}"
68
71
  local_image_pull_path = full_path(local_image_path) # Handle if domain is enabled
69
72
 
70
- # 0. test if an anonymous user cannot pull new content through the pull-through cache
71
- with anonymous_user, pytest.raises(CalledProcessError):
73
+ # 1. Pull new content anonymously from public caches; private caches require auth.
74
+ if pull_through_distribution.private:
75
+ with anonymous_user, pytest.raises(CalledProcessError):
76
+ local_registry.pull(local_image_pull_path)
72
77
  local_registry.pull(local_image_pull_path)
73
-
74
- # 1. pull remote content through the pull-through distribution
75
- local_registry.pull(local_image_pull_path)
78
+ else:
79
+ with anonymous_user:
80
+ local_registry.pull(local_image_pull_path)
76
81
  local_image = local_registry.inspect(local_image_pull_path)
77
82
 
78
83
  # when the client pulls the image, a repository, distribution, and remote is created in
@@ -119,9 +124,13 @@ def pull_and_verify(
119
124
  tags = container_tag_api.list(repository_version=repository.latest_version_href).results
120
125
  assert sorted(tags_to_verify) == sorted([tag.name for tag in tags])
121
126
 
122
- # 6. test if the anonymous user can pull existing content via the pull-through cache
123
- with anonymous_user:
124
- local_registry.pull(local_image_pull_path)
127
+ # 6. Cached content must retain the distribution's access restrictions.
128
+ if pull_through_distribution.private:
129
+ with anonymous_user, pytest.raises(CalledProcessError):
130
+ local_registry.pull(local_image_pull_path)
131
+ else:
132
+ with anonymous_user:
133
+ local_registry.pull(local_image_pull_path)
125
134
 
126
135
  return _pull_and_verify
127
136
 
@@ -134,14 +143,18 @@ def test_pull_through_distribution_uses_internal_base_path(pull_through_distribu
134
143
  assert distribution.pulp_labels[PULL_THROUGH_DISTRIBUTION_LABEL] != distribution.name
135
144
 
136
145
 
137
- def test_manifest_list_pull(delete_orphans_pre, pull_through_distribution, pull_and_verify):
146
+ @pytest.mark.parametrize("private", [False, True])
147
+ def test_manifest_list_pull(
148
+ delete_orphans_pre, pull_through_distribution, pull_and_verify, private
149
+ ):
138
150
  images = [f"{PULP_HELLO_WORLD_REPO}:latest", f"{PULP_HELLO_WORLD_REPO}:linux"]
139
- pull_and_verify(images, pull_through_distribution())
151
+ pull_and_verify(images, pull_through_distribution(private=private))
140
152
 
141
153
 
142
- def test_manifest_pull(delete_orphans_pre, pull_through_distribution, pull_and_verify):
154
+ @pytest.mark.parametrize("private", [False, True])
155
+ def test_manifest_pull(delete_orphans_pre, pull_through_distribution, pull_and_verify, private):
143
156
  images = [f"{PULP_FIXTURE_1}:manifest_a", f"{PULP_FIXTURE_1}:manifest_b"]
144
- pull_and_verify(images, pull_through_distribution())
157
+ pull_and_verify(images, pull_through_distribution(private=private))
145
158
 
146
159
 
147
160
  def test_pull_by_digest(
@@ -153,34 +166,47 @@ def test_pull_by_digest(
153
166
  full_path,
154
167
  gen_user,
155
168
  ):
169
+ if settings.TOKEN_AUTH_DISABLED:
170
+ pytest.skip("RBAC cannot be tested when token authentication is disabled")
171
+
156
172
  # THIS TEST FAILS WHEN THE CONTENT IS ALREADY PRESENT IN PULP!!
157
173
  image1 = f"{PULP_HELLO_WORLD_REPO}@{PULP_HELLO_WORLD_LINUX_AMD64_DIGEST}"
158
174
  image2 = f"{PULP_FIXTURE_1}@{PULP_FIXTURE_1_MANIFEST_A_DIGEST}"
159
175
  distribution1 = pull_through_distribution()
160
- distribution2 = pull_through_distribution()
176
+ distribution2 = pull_through_distribution(private=True)
161
177
  path1 = distribution1.pulp_labels[PULL_THROUGH_DISTRIBUTION_LABEL]
162
178
  path2 = distribution2.pulp_labels[PULL_THROUGH_DISTRIBUTION_LABEL]
163
179
  local_image_path1 = f"{full_path(path1)}/{image1}"
164
180
  local_image_path2 = f"{full_path(path2)}/{image2}"
165
181
 
166
- with anonymous_user, pytest.raises(CalledProcessError):
182
+ with anonymous_user:
167
183
  local_registry.pull(local_image_path1)
168
184
 
169
- local_registry.pull(local_image_path1)
170
-
171
185
  add_pull_through_entities_to_cleanup(local_image_path1.split("@")[0])
172
186
 
173
187
  with anonymous_user:
174
188
  local_registry.pull(local_image_path1)
175
189
 
176
- with gen_user():
177
- local_registry.pull(local_image_path1)
178
-
179
- with gen_user(), pytest.raises(CalledProcessError):
190
+ consumer = gen_user(
191
+ object_roles=[
192
+ ("container.containerpullthroughdistribution_consumer", distribution2.pulp_href)
193
+ ]
194
+ )
195
+ unprivileged_user = gen_user()
196
+ with anonymous_user, pytest.raises(CalledProcessError):
197
+ local_registry.pull(local_image_path2)
198
+ with unprivileged_user, pytest.raises(CalledProcessError):
199
+ local_registry.pull(local_image_path2)
200
+ with consumer:
180
201
  local_registry.pull(local_image_path2)
181
202
 
182
- with gen_user(model_roles=["container.containernamespace_collaborator"]):
183
- local_registry.pull(local_image_path1)
203
+ add_pull_through_entities_to_cleanup(local_image_path2.split("@")[0])
204
+
205
+ with anonymous_user, pytest.raises(CalledProcessError):
206
+ local_registry.pull(local_image_path2)
207
+ with unprivileged_user, pytest.raises(CalledProcessError):
208
+ local_registry.pull(local_image_path2)
209
+ with consumer:
184
210
  local_registry.pull(local_image_path2)
185
211
 
186
212
 
@@ -0,0 +1,33 @@
1
+ import pytest
2
+
3
+ from pulp_container.app.downloaders import RegistryAuthHttpDownloader
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ "header,expected",
8
+ [
9
+ (
10
+ 'Bearer realm="https://auth.docker.io/token",service="registry.docker.io"',
11
+ {"realm": "https://auth.docker.io/token", "service": "registry.docker.io"},
12
+ ),
13
+ (
14
+ 'Bearer realm="https://gcr.io/v2/token",service=gcr.io',
15
+ {"realm": "https://gcr.io/v2/token", "service": "gcr.io"},
16
+ ),
17
+ (
18
+ 'Bearer realm="https://quay.io/v2/auth",service="quay.io",'
19
+ 'scope="repository:foo/bar:pull,push"',
20
+ {
21
+ "realm": "https://quay.io/v2/auth",
22
+ "service": "quay.io",
23
+ "scope": "repository:foo/bar:pull,push",
24
+ },
25
+ ),
26
+ (
27
+ 'Bearer realm="https://example.com/token", service=example.com, error="a \\"b\\""',
28
+ {"realm": "https://example.com/token", "service": "example.com", "error": 'a "b"'},
29
+ ),
30
+ ],
31
+ )
32
+ def test_parse_auth_challenge(header, expected):
33
+ assert RegistryAuthHttpDownloader.parse_auth_challenge(header) == expected
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pulp-container
3
- Version: 2.29.2
3
+ Version: 2.29.3
4
4
  Summary: Container plugin for the Pulp Project
5
5
  Author-email: Pulp Team <pulp-list@redhat.com>
6
6
  Project-URL: Homepage, https://pulpproject.org
@@ -148,6 +148,7 @@ pulp_container/tests/functional/api/test_sync_signatures.py
148
148
  pulp_container/tests/functional/api/test_tagging_images.py
149
149
  pulp_container/tests/functional/api/test_token_authentication.py
150
150
  pulp_container/tests/unit/__init__.py
151
+ pulp_container/tests/unit/test_downloaders.py
151
152
  pulp_container/tests/unit/test_json_schemas.py
152
153
  pulp_container/tests/unit/test_models.py
153
154
  pulp_container/tests/unit/test_pull_through_repair.py
@@ -7,7 +7,7 @@ build-backend = 'setuptools.build_meta'
7
7
 
8
8
  [project]
9
9
  name = "pulp-container"
10
- version = "2.29.2"
10
+ version = "2.29.3"
11
11
  description = "Container plugin for the Pulp Project"
12
12
  readme = "README.md"
13
13
  authors = [
@@ -89,7 +89,7 @@ ignore = [
89
89
  [tool.bumpversion]
90
90
  # This section is managed by the plugin template. Do not edit manually.
91
91
 
92
- current_version = "2.29.2"
92
+ current_version = "2.29.3"
93
93
  commit = false
94
94
  tag = false
95
95
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<alpha>0a)?(?P<patch>\\d+)(\\.(?P<release>[a-z]+))?"
File without changes