Skip to content

Commit c86f61f

Browse files
authored
Merge pull request #88 from pngmbh/upstream-image-access-check
check image access on `deis pull`
2 parents 88c2ae3 + 1ce3bc5 commit c86f61f

File tree

12 files changed

+82
-7
lines changed

12 files changed

+82
-7
lines changed

rootfs/api/models/app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,9 @@ def deploy(self, release, force_deploy=False, rollback_on_failure=True): # noqa
520520
image = settings.SLUGRUNNER_IMAGE if release.build.type == 'buildpack' else release.image
521521

522522
try:
523+
# check access to the image, so users can't exploit the k8s image cache
524+
# to gain access to other users' images
525+
release.check_image_access()
523526
# create the application config in k8s (secret in this case) for all deploy objects
524527
self.set_application_config(release)
525528
# only buildpack apps need access to object storage

rootfs/api/models/release.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.conf import settings
44
from django.db import models
55

6-
from registry import publish_release, get_port as docker_get_port, RegistryException
6+
from registry import publish_release, get_port as docker_get_port, check_access as docker_check_access, RegistryException # noqa
77
from api.utils import dict_diff
88
from api.models import UuidAuditedModel
99
from api.exceptions import DeisException, AlreadyExists
@@ -136,6 +136,13 @@ def publish(self):
136136
deis_registry = bool(self.build.source_based)
137137
publish_release(source_image, self.image, deis_registry, self.get_registry_auth())
138138

139+
def check_image_access(self):
140+
try:
141+
creds = self.get_registry_auth()
142+
docker_check_access(self.image, creds)
143+
except Exception as e:
144+
raise DeisException(str(e)) from e
145+
139146
def get_port(self):
140147
"""
141148
Get application port for a given release. If pulling from private registry

rootfs/api/tests/test_app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def _mock_run(*args, **kwargs):
3535
@requests_mock.Mocker(real_http=True, adapter=adapter)
3636
@mock.patch('api.models.release.publish_release', lambda *args: None)
3737
@mock.patch('api.models.release.docker_get_port', mock_port)
38+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
3839
class AppTest(DeisTestCase):
3940
"""Tests creation of applications"""
4041

rootfs/api/tests/test_build.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
@requests_mock.Mocker(real_http=True, adapter=adapter)
2424
@mock.patch('api.models.release.publish_release', lambda *args: None)
2525
@mock.patch('api.models.release.docker_get_port', mock_port)
26+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
2627
class BuildTest(DeisTransactionTestCase):
2728

2829
"""Tests build notification from build system"""
@@ -591,6 +592,44 @@ def test_build_image_in_registry_with_auth(self, mock_requests):
591592
response = self.client.post(url, body)
592593
self.assertEqual(response.status_code, 201, response.data)
593594

595+
def test_build_image_no_registry_password(self, mock_requests):
596+
"""build with image from private registry, but no password given"""
597+
app_id = self.create_app()
598+
599+
# post an image as a build
600+
with mock.patch('api.models.release.docker_check_access') as mock_check_access:
601+
mock_check_access.side_effect = Exception('no no no') # let the image access fail
602+
url = "/v2/apps/{app_id}/builds".format(**locals())
603+
image = 'autotest/example'
604+
response = self.client.post(url, {'image': image})
605+
self.assertEqual(response.status_code, 400, response.data)
606+
607+
def test_build_image_wrong_registry_password(self, mock_requests):
608+
"""build with image from private registry, but wrong password given"""
609+
app_id = self.create_app()
610+
611+
# post an image as a build using registry hostname
612+
url = "/v2/apps/{app_id}/builds".format(**locals())
613+
image = 'autotest/example'
614+
response = self.client.post(url, {'image': image})
615+
self.assertEqual(response.status_code, 201, response.data)
616+
617+
# add the required PORT information
618+
url = '/v2/apps/{app_id}/config'.format(**locals())
619+
body = {'values': json.dumps({'PORT': '80'})}
620+
response = self.client.post(url, body)
621+
self.assertEqual(response.status_code, 201, response.data)
622+
623+
# set some registry information
624+
with mock.patch('api.models.release.docker_check_access') as mock_check_access:
625+
mock_check_access.side_effect = Exception('no no no') # let the image access fail
626+
url = '/v2/apps/{app_id}/config'.format(**locals())
627+
body = {'registry': json.dumps({'username': 'bob', 'password': 'zoomzoom'})}
628+
response = self.client.post(url, body)
629+
self.assertEqual(response.status_code, 400, response.data)
630+
mock_check_access.assert_called_with(
631+
image, {'username': 'bob', 'password': 'zoomzoom', 'email': 'autotest@deis.io'})
632+
594633
def test_build_image_in_registry_with_auth_no_port(self, mock_requests):
595634
"""add authentication to the build but with no PORT config"""
596635
app_id = self.create_app()

rootfs/api/tests/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
@requests_mock.Mocker(real_http=True, adapter=adapter)
2222
@mock.patch('api.models.release.publish_release', lambda *args: None)
2323
@mock.patch('api.models.release.docker_get_port', mock_port)
24+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
2425
class ConfigTest(DeisTransactionTestCase):
2526
"""Tests setting and updating config values"""
2627

rootfs/api/tests/test_healthchecks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
@requests_mock.Mocker(real_http=True, adapter=adapter)
1414
@mock.patch('api.models.release.publish_release', lambda *args: None)
1515
@mock.patch('api.models.release.docker_get_port', mock_port)
16+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
1617
class TestHealthchecks(DeisTransactionTestCase):
1718
"""Tests setting and updating config values"""
1819

rootfs/api/tests/test_hooks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
@requests_mock.Mocker(real_http=True, adapter=adapter)
5151
@mock.patch('api.models.release.publish_release', lambda *args: None)
5252
@mock.patch('api.models.release.docker_get_port', mock_port)
53+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
5354
class HookTest(DeisTransactionTestCase):
5455

5556
"""Tests API hooks used to trigger actions from external components"""

rootfs/api/tests/test_pods.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
@requests_mock.Mocker(real_http=True, adapter=adapter)
2222
@mock.patch('api.models.release.publish_release', lambda *args: None)
2323
@mock.patch('api.models.release.docker_get_port', mock_port)
24+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
2425
class PodTest(DeisTransactionTestCase):
2526
"""Tests creation of pods on nodes"""
2627

@@ -322,7 +323,7 @@ def test_container_str(self, mock_requests):
322323
self.assertIn(pod['type'], ['web', 'worker'])
323324
self.assertEqual(pod['release'], 'v2')
324325
# pod name is auto generated so use regex
325-
self.assertRegex(pod['name'], app_id + '-(worker|web)-[0-9]{8,10}-[a-z0-9]{5}')
326+
self.assertRegex(pod['name'], app_id + '-(worker|web)-[0-9]{7,10}-[a-z0-9]{5}')
326327

327328
def test_pod_command_format(self, mock_requests):
328329
# regression test for https://github.com/deisthree/deis/pull/1285

rootfs/api/tests/test_registry.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
import requests_mock
3+
from unittest import mock
34

45
from django.core.cache import cache
56
from django.contrib.auth.models import User
@@ -136,7 +137,11 @@ def test_registry_deploy(self, mock_requests):
136137
self.assertEqual(response.data['registry']['password'], 's3cur3pw1')
137138

138139
# post a new build
139-
url = "/v2/apps/{app_id}/builds".format(**locals())
140-
body = {'image': 'autotest/example'}
141-
response = self.client.post(url, body)
142-
self.assertEqual(response.status_code, 201, response.data)
140+
with mock.patch('api.models.release.docker_check_access') as mock_check_access:
141+
url = "/v2/apps/{app_id}/builds".format(**locals())
142+
body = {'image': 'autotest/example'}
143+
response = self.client.post(url, body)
144+
self.assertEqual(response.status_code, 201, response.data)
145+
mock_check_access.assert_called_with(
146+
'autotest/example',
147+
{'password': 's3cur3pw1', 'username': 'bob', 'email': 'autotest@deis.io'})

rootfs/api/tests/test_release.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
@requests_mock.Mocker(real_http=True, adapter=adapter)
2222
@mock.patch('api.models.release.publish_release', lambda *args: None)
2323
@mock.patch('api.models.release.docker_get_port', mock_port)
24+
@mock.patch('api.models.release.docker_check_access', lambda *args: None)
2425
class ReleaseTest(DeisTransactionTestCase):
2526

2627
"""Tests push notification from build system"""

rootfs/registry/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .dockerclient import publish_release, get_port, RegistryException # noqa
1+
from .dockerclient import publish_release, get_port, check_access, RegistryException # noqa

rootfs/registry/dockerclient.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ def inspect_image(self, target):
154154
# inspect the image
155155
return self.client.inspect_image(target)
156156

157+
def check_access(self, target, creds=None):
158+
"""
159+
Check access to the docker image, with the given creds (if any).
160+
Due to the k8s docker image cache, we can't rely on k8s to do this
161+
check - see https://github.com/teamhephy/workflow/issues/78
162+
"""
163+
name, _ = docker.utils.parse_repository_tag(target)
164+
self.login(name, creds)
165+
self.inspect_image(target)
166+
# no exception == success
167+
157168

158169
def check_blacklist(repo):
159170
"""Check a Docker repository name for collision with deis/* components."""
@@ -197,3 +208,7 @@ def publish_release(source, target, deis_registry, creds=None):
197208

198209
def get_port(target, deis_registry, creds=None):
199210
return DockerClient().get_port(target, deis_registry, creds)
211+
212+
213+
def check_access(target, creds=None):
214+
return DockerClient().check_access(target, creds)

0 commit comments

Comments
 (0)