-
Notifications
You must be signed in to change notification settings - Fork 87
Add error_on_reject for partial package policy rejection #1307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added an `error_on_reject` boolean field to PythonRepository (default: `True`). When `False`, packages rejected by the blocklist or package substitution policies are skipped instead of failing the entire repository version. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ("python", "0023_packageyank"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="pythonrepository", | ||
| name="error_on_reject", | ||
| field=models.BooleanField(default=True), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| BaseModel, | ||
| Content, | ||
| Distribution, | ||
| ProgressReport, | ||
| Publication, | ||
| Remote, | ||
| Repository, | ||
|
|
@@ -394,6 +395,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin): | |
|
|
||
| autopublish = models.BooleanField(default=False) | ||
| allow_package_substitution = models.BooleanField(default=True) | ||
| error_on_reject = models.BooleanField(default=True) | ||
|
|
||
| class Meta: | ||
| default_related_name = "%(app_label)s_%(model_name)s" | ||
|
|
@@ -424,7 +426,8 @@ def finalize_new_version(self, new_version): | |
| Remove duplicate packages that have the same filename. | ||
|
|
||
| When allow_package_substitution is False, reject any new version that would implicitly | ||
| replace existing content with different checksums (content substitution). | ||
| replace existing content with different checksums (content substitution), unless | ||
| error_on_reject is False, in which case the conflicting packages are skipped. | ||
|
Comment on lines
428
to
+430
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mentions |
||
|
|
||
| Also checks newly added content against the repository's blocklist entries. | ||
| """ | ||
|
|
@@ -436,53 +439,123 @@ def finalize_new_version(self, new_version): | |
|
|
||
| def _check_for_package_substitution(self, new_version): | ||
| """ | ||
| Raise a ValidationError if newly added packages would replace existing packages | ||
| that have the same filename but a different sha256 checksum. | ||
| Handle packages that would replace existing packages with the same filename but a | ||
| different sha256 checksum. | ||
|
|
||
| When error_on_reject is True, raise a ValidationError. When False, remove the | ||
| newly added conflicting packages from the version and record them in a progress report. | ||
| """ | ||
| qs = PythonPackageContent.objects.filter(pk__in=new_version.content) | ||
| duplicates = collect_duplicates(qs, ("filename",)) | ||
| if duplicates: | ||
| if not duplicates: | ||
| return | ||
|
|
||
| if self.error_on_reject: | ||
| raise ValidationError( | ||
| "Found duplicate packages being added with the same filename but different " | ||
| "checksums. To allow this, set 'allow_package_substitution' to True on the " | ||
| f"repository. Conflicting packages: {duplicates}" | ||
| ) | ||
|
|
||
| added_content = PythonPackageContent.objects.filter( | ||
| pk__in=new_version.added(base_version=new_version.base_version) | ||
| ) | ||
| added_pks = {str(pkg.pk): pkg.filename for pkg in added_content.only("pk", "filename")} | ||
| to_remove_pks = [] | ||
| messages = [] | ||
| for dup in duplicates: | ||
| for pk in dup.duplicate_pks: | ||
| if pk in added_pks: | ||
| to_remove_pks.append(pk) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if we have two new packages with the same filename? Will be both marked for removal? |
||
| messages.append(f"{added_pks[pk]} ({pk})") | ||
|
|
||
| if to_remove_pks: | ||
| new_version.remove_content(PythonPackageContent.objects.filter(pk__in=to_remove_pks)) | ||
| self._report_rejected_packages( | ||
| messages, | ||
| message="Skipping packages rejected by package substitution policy", | ||
| code="python.reject.substitution", | ||
| ) | ||
|
|
||
| def _check_blocklist(self, new_version): | ||
| """ | ||
| Check newly added content in a repository version against the blocklist. | ||
|
|
||
| When error_on_reject is True, raise a ValidationError. When False, remove the | ||
| blocklisted packages from the version and record them in a progress report. | ||
| """ | ||
| added_content = PythonPackageContent.objects.filter( | ||
| pk__in=new_version.added().values_list("pk", flat=True) | ||
| ).only("filename", "name_normalized", "version") | ||
| if added_content.exists(): | ||
| self.check_blocklist_for_packages(added_content) | ||
| pk__in=new_version.added(base_version=new_version.base_version) | ||
| ).only("pk", "filename", "name_normalized", "version") | ||
| if not added_content.exists(): | ||
| return | ||
|
|
||
| def check_blocklist_for_packages(self, packages): | ||
| blocked = self.find_blocklisted_packages(added_content) | ||
| if not blocked: | ||
| return | ||
|
|
||
| if self.error_on_reject: | ||
| raise ValidationError( | ||
| "Blocklisted packages cannot be added to this repository: {}".format( | ||
| ", ".join(pkg.filename for pkg in blocked) | ||
| ) | ||
| ) | ||
|
|
||
| new_version.remove_content( | ||
| PythonPackageContent.objects.filter(pk__in=[p.pk for p in blocked]) | ||
| ) | ||
| self._report_rejected_packages( | ||
| [pkg.filename for pkg in blocked], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For package substitution we log filenames + pks, but for blocklist only filenames. Should we make it consistent? |
||
| message="Skipping packages rejected by blocklist policy", | ||
| code="python.reject.blocklist", | ||
| ) | ||
|
|
||
| def find_blocklisted_packages(self, packages): | ||
| """ | ||
| Raise a ValidationError if any of the given packages match a blocklist entry. | ||
| Return the packages from ``packages`` that match a blocklist entry. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double `` instead of ` |
||
| """ | ||
| entries = PythonBlocklistEntry.objects.filter(repository=self) | ||
| if not entries.exists(): | ||
| return | ||
| entries = list(PythonBlocklistEntry.objects.filter(repository=self)) | ||
| if not entries: | ||
| return [] | ||
|
|
||
| blocked = [] | ||
| for pkg in packages: | ||
| for entry in entries: | ||
| if entry.filename and entry.filename == pkg.filename: | ||
| blocked.append(pkg.filename) | ||
| blocked.append(pkg) | ||
| break | ||
| if entry.name == pkg.name_normalized: | ||
| if not entry.version or entry.version == pkg.version: | ||
| blocked.append(pkg.filename) | ||
| blocked.append(pkg) | ||
| break | ||
| return blocked | ||
|
|
||
| def check_blocklist_for_packages(self, packages): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you move content of |
||
| """ | ||
| Raise a ValidationError if any of the given packages match a blocklist entry. | ||
| """ | ||
| blocked = self.find_blocklisted_packages(packages) | ||
| if blocked: | ||
| raise ValidationError( | ||
| "Blocklisted packages cannot be added to this repository: {}".format( | ||
| ", ".join(blocked) | ||
| ", ".join(pkg.filename for pkg in blocked) | ||
| ) | ||
| ) | ||
|
|
||
| def _report_rejected_packages(self, details, message, code): | ||
| """ | ||
| Record skipped packages in a task progress report. | ||
| """ | ||
| suffix = "; ".join(details) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to store the same info in suffix as in logs? |
||
| log.info("%s: %s", message, suffix) | ||
| with ProgressReport( | ||
| message=message, | ||
| code=code, | ||
| total=len(details), | ||
| suffix=suffix, | ||
| ) as pb: | ||
| pb.increase_by(len(details)) | ||
|
|
||
|
|
||
| class PythonBlocklistEntry(BaseModel): | ||
| """ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
error-on-rejectis not implemented in the CLI yet. You can use httpie for now and change it back later once the CLI support is added.