Skip to content

Commit

Permalink
Add ContentRedirectContentGuard
Browse files Browse the repository at this point in the history
  • Loading branch information
mdellweg committed Jun 25, 2020
1 parent 5914501 commit 02564d7
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGES/6894.feature
@@ -0,0 +1 @@
Added ContentRedirectContentGuard to redirect with preauthenticated urls to the content app.
27 changes: 27 additions & 0 deletions pulp_container/app/migrations/0005_contentredirectcontentguard.py
@@ -0,0 +1,27 @@
# Generated by Django 2.2.12 on 2020-06-25 08:23

from django.db import migrations, models
import django.db.models.deletion
import pulp_container.app.models


class Migration(migrations.Migration):

dependencies = [
('core', '0033_increase_remote_artifact_size_field'),
('container', '0004_upload'),
]

operations = [
migrations.CreateModel(
name='ContentRedirectContentGuard',
fields=[
('contentguard_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, related_name='container_contentredirectcontentguard', serialize=False, to='core.ContentGuard')),
('shared_secret', models.BinaryField(default=pulp_container.app.models._gen_secret, max_length=32)),
],
options={
'default_related_name': '%(app_label)s_%(model_name)s',
},
bases=('core.contentguard',),
),
]
62 changes: 62 additions & 0 deletions pulp_container/app/models.py
Expand Up @@ -3,16 +3,19 @@
import re
import time
from logging import getLogger
from url_normalize import url_normalize

from django.db import models
from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.contrib.postgres import fields
from django.shortcuts import redirect

from pulpcore.plugin.download import DownloaderFactory
from pulpcore.plugin.models import (
Artifact,
Content,
ContentGuard,
BaseModel,
Remote,
Repository,
Expand Down Expand Up @@ -309,6 +312,14 @@ def get_repository_version(self):
else:
return None

def redirect_to_content_app(self, url):
"""
Add preauthentication query string to redirect attempt.
"""
if self.content_guard:
url = self.content_guard.cast().preauthenticate_url(url)
return redirect(url)

class Meta:
default_related_name = "%(app_label)s_%(model_name)s"

Expand Down Expand Up @@ -375,3 +386,54 @@ def append_chunk(self, chunk, chunk_size=None, save=True):
self.file.close() # Flush
for algorithm in Artifact.DIGEST_FIELDS:
setattr(self, algorithm, hashers[algorithm].hexdigest())


def _gen_secret():
return os.urandom(32)


class ContentRedirectContentGuard(ContentGuard):
"""
Content guard to allow preauthenticated redirects to the content app.
"""

TYPE = "content_redirect"

shared_secret = models.BinaryField(max_length=32, default=_gen_secret)

def permit(self, request):
"""
Permit preauthenticated redirects from pulp-api.
"""
signed_url = request.url
try:
validate_token = request.query["validate_token"]
except KeyError:
raise PermissionError("Access not authenticated")
hex_salt, hex_digest = validate_token.split(":", 1)
salt = bytes.fromhex(hex_salt)
digest = bytes.fromhex(hex_digest)
url = re.sub(r"\?validate_token=.*$", "", str(signed_url))
if not digest == self._get_digest(salt, url):
raise PermissionError("Access not authenticated")

def preauthenticate_url(self, url, salt=None):
"""
Add validate_token to urls query string.
"""
if not salt:
salt = _gen_secret()
hex_salt = salt.hex()
digest = self._get_digest(salt, url).hex()
url = url + f"?validate_token={hex_salt}:{digest}"
return url

def _get_digest(self, salt, url):
hasher = hashlib.sha256()
hasher.update(salt)
hasher.update(url_normalize(url).encode())
hasher.update(self.shared_secret)
return hasher.digest()

class Meta:
default_related_name = "%(app_label)s_%(model_name)s"
2 changes: 2 additions & 0 deletions pulp_container/app/registry.py
Expand Up @@ -131,6 +131,7 @@ async def get_tag(self, request):
path = request.match_info["path"]
tag_name = request.match_info["tag_name"]
distribution = self._match_distribution(path)
self._permit(request, distribution)
repository_version = distribution.get_repository_version()
accepted_media_types = await Registry.get_accepted_media_types(request)

Expand Down Expand Up @@ -239,6 +240,7 @@ async def get_by_digest(self, request):
path = request.match_info["path"]
digest = "sha256:{digest}".format(digest=request.match_info["digest"])
distribution = self._match_distribution(path)
self._permit(request, distribution)
repository_version = distribution.get_repository_version()
log.info(digest)
try:
Expand Down
39 changes: 39 additions & 0 deletions pulp_container/app/serializers.py
Expand Up @@ -2,6 +2,7 @@
import os

from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist

from rest_framework import serializers

Expand All @@ -11,6 +12,7 @@
RepositoryVersion,
)
from pulpcore.plugin.serializers import (
ContentGuardSerializer,
DetailRelatedField,
NestedRelatedField,
RelatedField,
Expand Down Expand Up @@ -162,6 +164,34 @@ class ContainerDistributionSerializer(RepositoryVersionDistributionSerializer):
"this distribution."
),
)
content_guard = DetailRelatedField(
required=False,
help_text=_("An optional content-guard. If none is specified, a default one will be used."),
view_name=r"contentguards-container/content-redirect-detail",
queryset=models.ContentRedirectContentGuard.objects.all(),
allow_null=False,
)

def validate(self, data):
"""
Validate the ContainterDistribution.
Make sure there is an instance of ContentRedirectContentGuard always present in validated
data.
"""
validated_data = super().validate(data)
if "content_guard" not in validated_data:
try:
validated_data["content_guard"] = models.ContentRedirectContentGuard.objects.get(
name="content redirect"
)
except ObjectDoesNotExist:
cg_serializer = ContentRedirectContentGuardSerializer(
data={"name": "content redirect"}
)
cg_serializer.is_valid(raise_exception=True)
validated_data["content_guard"] = cg_serializer.create(cg_serializer.validated_data)
return validated_data

class Meta:
model = models.ContainerDistribution
Expand All @@ -170,6 +200,15 @@ class Meta:
)


class ContentRedirectContentGuardSerializer(ContentGuardSerializer):
"""
A serializer for ContentRedirectContentGuard.
"""

class Meta(ContentGuardSerializer.Meta):
model = models.ContentRedirectContentGuard


class TagOperationSerializer(serializers.Serializer):
"""
A base serializer for tagging and untagging manifests.
Expand Down
23 changes: 17 additions & 6 deletions pulp_container/app/viewsets.py
Expand Up @@ -20,7 +20,6 @@

from django.conf import settings
from django.core.files.base import ContentFile
from django.http import HttpResponseRedirect

from pulpcore.plugin.serializers import (
AsyncOperationResponseSerializer,
Expand All @@ -32,6 +31,7 @@
BaseDistributionViewSet,
CharInFilter,
ContentFilter,
ContentGuardViewSet,
NamedModelViewSet,
ReadOnlyContentViewSet,
RemoteViewSet,
Expand All @@ -56,6 +56,7 @@ class ContainerRegistryApiMixin:
"""
Mixin to add docker registry specific headers to all error responses.
"""

def handle_exception(self, exc):
"""
Add docker registry specific headers to all error responses.
Expand Down Expand Up @@ -445,6 +446,16 @@ class ContainerDistributionViewSet(BaseDistributionViewSet):
serializer_class = serializers.ContainerDistributionSerializer


class ContentRedirectContentGuardViewSet(ContentGuardViewSet):
"""
Content guard to protect preauthenticated redirects to the content app.
"""

endpoint_name = "content_redirect"
queryset = models.ContentRedirectContentGuard.objects.all()
serializer_class = serializers.ContentRedirectContentGuardSerializer


class ManifestRenderer(BaseRenderer):
"""
Rendered class for rendering Manifest responses.
Expand Down Expand Up @@ -773,8 +784,8 @@ def get(self, request, path, pk=None):
else:
raise Http404("Repository {} does not exist.".format(path))
blob = get_object_or_404(models.Blob, digest=pk, pk__in=repository_version.content)
return HttpResponseRedirect(
"{}/pulp/container/{}/blobs/{}".format(settings.CONTENT_ORIGIN, path, blob.digest)
return distribution.redirect_to_content_app(
"{}/pulp/container/{}/blobs/{}".format(settings.CONTENT_ORIGIN, path, blob.digest),
)


Expand Down Expand Up @@ -839,10 +850,10 @@ def get(self, request, path, pk=None):
models.Manifest, digest=pk, pk__in=repository_version.content
)

return HttpResponseRedirect(
return distribution.redirect_to_content_app(
"{}/pulp/container/{}/manifests/{}".format(
settings.CONTENT_ORIGIN, path, manifest.digest
)
settings.CONTENT_ORIGIN, path, manifest.digest,
),
)

def put(self, request, path, pk=None):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -2,3 +2,4 @@ pulpcore>=3.4
ecdsa~=0.13.2
pyjwkest~=1.4.0
pyjwt[crypto]~=1.7.1
url-normalize~=1.4.2

0 comments on commit 02564d7

Please sign in to comment.