|
7 | 7 | from typing import List, TypedDict |
8 | 8 | from urllib.parse import urljoin |
9 | 9 |
|
| 10 | +from cryptography.x509 import load_pem_x509_certificate |
10 | 11 | from django.conf import settings |
11 | 12 | from django.core.validators import URLValidator |
12 | 13 | from django.core.exceptions import ObjectDoesNotExist |
@@ -582,3 +583,204 @@ def validate(self, data): |
582 | 583 | data = super().validate(data) |
583 | 584 | data["value"] = self.context["content_object"].pulp_labels[data["key"]] |
584 | 585 | return data |
| 586 | + |
| 587 | + |
| 588 | +class RemoteNetworkConfigSerializer(serializers.Serializer): |
| 589 | + """ |
| 590 | + Shared network configuration fields and validation logic used by both |
| 591 | + RemoteSerializer and UploadSerializerFieldsMixin. |
| 592 | + """ |
| 593 | + |
| 594 | + ca_cert = serializers.CharField( |
| 595 | + help_text="A PEM encoded CA certificate used to validate the server " |
| 596 | + "certificate presented by the remote server.", |
| 597 | + required=False, |
| 598 | + allow_null=True, |
| 599 | + ) |
| 600 | + client_cert = serializers.CharField( |
| 601 | + help_text="A PEM encoded client certificate used for authentication.", |
| 602 | + required=False, |
| 603 | + allow_null=True, |
| 604 | + ) |
| 605 | + client_key = serializers.CharField( |
| 606 | + help_text="A PEM encoded private key used for authentication.", |
| 607 | + required=False, |
| 608 | + allow_null=True, |
| 609 | + write_only=True, |
| 610 | + ) |
| 611 | + tls_validation = serializers.BooleanField( |
| 612 | + help_text="If True, TLS peer validation must be performed.", required=False |
| 613 | + ) |
| 614 | + proxy_url = serializers.CharField( |
| 615 | + help_text="The proxy URL. Format: scheme://host:port", |
| 616 | + required=False, |
| 617 | + allow_null=True, |
| 618 | + ) |
| 619 | + proxy_username = serializers.CharField( |
| 620 | + help_text="The username to authenticte to the proxy.", |
| 621 | + required=False, |
| 622 | + allow_null=True, |
| 623 | + write_only=True, |
| 624 | + ) |
| 625 | + proxy_password = serializers.CharField( |
| 626 | + help_text=_( |
| 627 | + "The password to authenticate to the proxy. Extra leading and trailing whitespace " |
| 628 | + "characters are not trimmed." |
| 629 | + ), |
| 630 | + required=False, |
| 631 | + allow_null=True, |
| 632 | + write_only=True, |
| 633 | + trim_whitespace=False, |
| 634 | + style={"input_type": "password"}, |
| 635 | + ) |
| 636 | + username = serializers.CharField( |
| 637 | + help_text="The username to be used for authentication when syncing.", |
| 638 | + required=False, |
| 639 | + allow_null=True, |
| 640 | + write_only=True, |
| 641 | + ) |
| 642 | + password = serializers.CharField( |
| 643 | + help_text=_( |
| 644 | + "The password to be used for authentication when syncing. Extra leading and trailing " |
| 645 | + "whitespace characters are not trimmed." |
| 646 | + ), |
| 647 | + required=False, |
| 648 | + allow_null=True, |
| 649 | + write_only=True, |
| 650 | + trim_whitespace=False, |
| 651 | + style={"input_type": "password"}, |
| 652 | + ) |
| 653 | + max_retries = serializers.IntegerField( |
| 654 | + help_text=( |
| 655 | + "Maximum number of retry attempts after a download failure. If not set then the " |
| 656 | + "default value (3) will be used." |
| 657 | + ), |
| 658 | + required=False, |
| 659 | + allow_null=True, |
| 660 | + ) |
| 661 | + total_timeout = serializers.FloatField( |
| 662 | + allow_null=True, |
| 663 | + required=False, |
| 664 | + help_text=( |
| 665 | + "aiohttp.ClientTimeout.total (q.v.) for download-connections. The default is null, " |
| 666 | + "which will cause the default from the aiohttp library to be used." |
| 667 | + ), |
| 668 | + min_value=0.0, |
| 669 | + ) |
| 670 | + connect_timeout = serializers.FloatField( |
| 671 | + allow_null=True, |
| 672 | + required=False, |
| 673 | + help_text=( |
| 674 | + "aiohttp.ClientTimeout.connect (q.v.) for download-connections. The default is null, " |
| 675 | + "which will cause the default from the aiohttp library to be used." |
| 676 | + ), |
| 677 | + min_value=0.0, |
| 678 | + ) |
| 679 | + sock_connect_timeout = serializers.FloatField( |
| 680 | + allow_null=True, |
| 681 | + required=False, |
| 682 | + help_text=( |
| 683 | + "aiohttp.ClientTimeout.sock_connect (q.v.) for download-connections. The default is " |
| 684 | + "null, which will cause the default from the aiohttp library to be used." |
| 685 | + ), |
| 686 | + min_value=0.0, |
| 687 | + ) |
| 688 | + sock_read_timeout = serializers.FloatField( |
| 689 | + allow_null=True, |
| 690 | + required=False, |
| 691 | + help_text=( |
| 692 | + "aiohttp.ClientTimeout.sock_read (q.v.) for download-connections. The default is " |
| 693 | + "null, which will cause the default from the aiohttp library to be used." |
| 694 | + ), |
| 695 | + min_value=0.0, |
| 696 | + ) |
| 697 | + headers = serializers.ListField( |
| 698 | + child=serializers.DictField(), |
| 699 | + help_text=_("Headers for aiohttp.Clientsession"), |
| 700 | + required=False, |
| 701 | + ) |
| 702 | + |
| 703 | + def validate_proxy_url(self, value): |
| 704 | + """ |
| 705 | + Check, that the proxy_url does not contain credentials. |
| 706 | + """ |
| 707 | + if value and "@" in value: |
| 708 | + raise serializers.ValidationError(_("proxy_url must not contain credentials")) |
| 709 | + return value |
| 710 | + |
| 711 | + def validate_ca_cert(self, value): |
| 712 | + return self._validate_certificate("ca_cert", value) |
| 713 | + |
| 714 | + def validate_client_cert(self, value): |
| 715 | + return self._validate_certificate("client_cert", value) |
| 716 | + |
| 717 | + @staticmethod |
| 718 | + def _validate_certificate(which_cert, value): |
| 719 | + """ |
| 720 | + Validate and return *just* the certs and not any commentary that came along with them. |
| 721 | +
|
| 722 | + Args: |
| 723 | + which_cert: The attribute-name whose cert we're validating |
| 724 | + (only used for error-message). |
| 725 | + value: The string being proposed as a certificate-containing PEM. |
| 726 | +
|
| 727 | + Raises: |
| 728 | + ValidationError: When the provided value has no or an invalid certificate. |
| 729 | +
|
| 730 | + Returns: |
| 731 | + The pem-string with *just* the validated BEGIN/END CERTIFICATE segments. |
| 732 | + """ |
| 733 | + if value: |
| 734 | + try: |
| 735 | + # Find any/all CERTIFICATE entries in the proposed PEM and let crypto validate them. |
| 736 | + # NOTE: crypto/39 includes load_certificates(), which will let us remove this whole |
| 737 | + # loop. But we want to fix the current problem on older supported branches that |
| 738 | + # allow 38, so we do it ourselves for now |
| 739 | + certs = list() |
| 740 | + a_cert = "" |
| 741 | + for line in value.split("\n"): |
| 742 | + if "-----BEGIN CERTIFICATE-----" in line or a_cert: |
| 743 | + a_cert += line + "\n" |
| 744 | + if "-----END CERTIFICATE-----" in line: |
| 745 | + load_pem_x509_certificate(bytes(a_cert, "ASCII")) |
| 746 | + certs.append(a_cert.strip()) |
| 747 | + a_cert = "" |
| 748 | + if not certs: |
| 749 | + raise serializers.ValidationError( |
| 750 | + "No {} specified in string {}".format(which_cert, value) |
| 751 | + ) |
| 752 | + return "\n".join(certs) + "\n" |
| 753 | + except ValueError as e: |
| 754 | + raise serializers.ValidationError( |
| 755 | + "Invalid {} specified, error '{}'".format(which_cert, e.args) |
| 756 | + ) |
| 757 | + |
| 758 | + def validate(self, data): |
| 759 | + """ |
| 760 | + Check that proxy credentials are only provided completely and if a proxy is configured. |
| 761 | + Adapted to work for both ModelSerializers (Remotes) and standard Serializers (Uploads). |
| 762 | + """ |
| 763 | + # Handle cases where we don't have an instance (e.g. Uploads) |
| 764 | + instance = getattr(self, "instance", None) |
| 765 | + partial = getattr(self, "partial", False) |
| 766 | + |
| 767 | + proxy_url = instance.proxy_url if instance and partial else None |
| 768 | + proxy_url = data.get("proxy_url", proxy_url) |
| 769 | + |
| 770 | + proxy_username = instance.proxy_username if instance and partial else None |
| 771 | + proxy_username = data.get("proxy_username", proxy_username) |
| 772 | + |
| 773 | + proxy_password = instance.proxy_password if instance and partial else None |
| 774 | + proxy_password = data.get("proxy_password", proxy_password) |
| 775 | + |
| 776 | + if (proxy_username or proxy_password) and not proxy_url: |
| 777 | + raise serializers.ValidationError( |
| 778 | + _("proxy credentials cannot be specified without a proxy") |
| 779 | + ) |
| 780 | + |
| 781 | + if bool(proxy_username) is not bool(proxy_password): |
| 782 | + raise serializers.ValidationError( |
| 783 | + _("proxy username and password can only be specified together") |
| 784 | + ) |
| 785 | + |
| 786 | + return data |
0 commit comments