Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/7201.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add network configuration support for synchronous file uploads
202 changes: 202 additions & 0 deletions pulpcore/app/serializers/base.py
Copy link
Member

Choose a reason for hiding this comment

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

Circular import between pulpcore/app/serializers/base.py and pulpcore.app.serializers.repository.
Move validate_certificate elsewhere.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from typing import List, TypedDict
from urllib.parse import urljoin

from cryptography.x509 import load_pem_x509_certificate
from django.conf import settings
from django.core.validators import URLValidator
from django.core.exceptions import ObjectDoesNotExist
Expand Down Expand Up @@ -582,3 +583,204 @@ def validate(self, data):
data = super().validate(data)
data["value"] = self.context["content_object"].pulp_labels[data["key"]]
return data


class RemoteNetworkConfigSerializer(serializers.Serializer):
"""
Shared network configuration fields and validation logic used by both
RemoteSerializer and UploadSerializerFieldsMixin.
"""

ca_cert = serializers.CharField(
help_text="A PEM encoded CA certificate used to validate the server "
"certificate presented by the remote server.",
required=False,
allow_null=True,
)
client_cert = serializers.CharField(
help_text="A PEM encoded client certificate used for authentication.",
required=False,
allow_null=True,
)
client_key = serializers.CharField(
help_text="A PEM encoded private key used for authentication.",
required=False,
allow_null=True,
write_only=True,
)
tls_validation = serializers.BooleanField(
help_text="If True, TLS peer validation must be performed.", required=False
)
proxy_url = serializers.CharField(
help_text="The proxy URL. Format: scheme://host:port",
required=False,
allow_null=True,
)
proxy_username = serializers.CharField(
help_text="The username to authenticte to the proxy.",
required=False,
allow_null=True,
write_only=True,
)
proxy_password = serializers.CharField(
help_text=_(
"The password to authenticate to the proxy. Extra leading and trailing whitespace "
"characters are not trimmed."
),
required=False,
allow_null=True,
write_only=True,
trim_whitespace=False,
style={"input_type": "password"},
)
username = serializers.CharField(
help_text="The username to be used for authentication when syncing.",
required=False,
allow_null=True,
write_only=True,
)
password = serializers.CharField(
help_text=_(
"The password to be used for authentication when syncing. Extra leading and trailing "
"whitespace characters are not trimmed."
),
required=False,
allow_null=True,
write_only=True,
trim_whitespace=False,
style={"input_type": "password"},
)
max_retries = serializers.IntegerField(
help_text=(
"Maximum number of retry attempts after a download failure. If not set then the "
"default value (3) will be used."
),
required=False,
allow_null=True,
)
total_timeout = serializers.FloatField(
allow_null=True,
required=False,
help_text=(
"aiohttp.ClientTimeout.total (q.v.) for download-connections. The default is null, "
"which will cause the default from the aiohttp library to be used."
),
min_value=0.0,
)
connect_timeout = serializers.FloatField(
allow_null=True,
required=False,
help_text=(
"aiohttp.ClientTimeout.connect (q.v.) for download-connections. The default is null, "
"which will cause the default from the aiohttp library to be used."
),
min_value=0.0,
)
sock_connect_timeout = serializers.FloatField(
allow_null=True,
required=False,
help_text=(
"aiohttp.ClientTimeout.sock_connect (q.v.) for download-connections. The default is "
"null, which will cause the default from the aiohttp library to be used."
),
min_value=0.0,
)
sock_read_timeout = serializers.FloatField(
allow_null=True,
required=False,
help_text=(
"aiohttp.ClientTimeout.sock_read (q.v.) for download-connections. The default is "
"null, which will cause the default from the aiohttp library to be used."
),
min_value=0.0,
)
headers = serializers.ListField(
child=serializers.DictField(),
help_text=_("Headers for aiohttp.Clientsession"),
required=False,
)

def validate_proxy_url(self, value):
"""
Check, that the proxy_url does not contain credentials.
"""
if value and "@" in value:
raise serializers.ValidationError(_("proxy_url must not contain credentials"))
return value

def validate_ca_cert(self, value):
return self._validate_certificate("ca_cert", value)

def validate_client_cert(self, value):
return self._validate_certificate("client_cert", value)

@staticmethod
def _validate_certificate(which_cert, value):
"""
Validate and return *just* the certs and not any commentary that came along with them.

Args:
which_cert: The attribute-name whose cert we're validating
(only used for error-message).
value: The string being proposed as a certificate-containing PEM.

Raises:
ValidationError: When the provided value has no or an invalid certificate.

Returns:
The pem-string with *just* the validated BEGIN/END CERTIFICATE segments.
"""
if value:
try:
# Find any/all CERTIFICATE entries in the proposed PEM and let crypto validate them.
# NOTE: crypto/39 includes load_certificates(), which will let us remove this whole
# loop. But we want to fix the current problem on older supported branches that
# allow 38, so we do it ourselves for now
certs = list()
a_cert = ""
for line in value.split("\n"):
if "-----BEGIN CERTIFICATE-----" in line or a_cert:
a_cert += line + "\n"
if "-----END CERTIFICATE-----" in line:
load_pem_x509_certificate(bytes(a_cert, "ASCII"))
certs.append(a_cert.strip())
a_cert = ""
if not certs:
raise serializers.ValidationError(
"No {} specified in string {}".format(which_cert, value)
)
return "\n".join(certs) + "\n"
except ValueError as e:
raise serializers.ValidationError(
"Invalid {} specified, error '{}'".format(which_cert, e.args)
)

def validate(self, data):
"""
Check that proxy credentials are only provided completely and if a proxy is configured.
Adapted to work for both ModelSerializers (Remotes) and standard Serializers (Uploads).
"""
# Handle cases where we don't have an instance (e.g. Uploads)
instance = getattr(self, "instance", None)
partial = getattr(self, "partial", False)

proxy_url = instance.proxy_url if instance and partial else None
proxy_url = data.get("proxy_url", proxy_url)

proxy_username = instance.proxy_username if instance and partial else None
proxy_username = data.get("proxy_username", proxy_username)

proxy_password = instance.proxy_password if instance and partial else None
proxy_password = data.get("proxy_password", proxy_password)

if (proxy_username or proxy_password) and not proxy_url:
raise serializers.ValidationError(
_("proxy credentials cannot be specified without a proxy")
)

if bool(proxy_username) is not bool(proxy_password):
raise serializers.ValidationError(
_("proxy username and password can only be specified together")
)

return data
Loading
Loading