Skip to content
This repository has been archived by the owner on May 6, 2020. It is now read-only.

Commit

Permalink
feat(api): add deis tls (#1004)
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Fisher committed Aug 26, 2016
1 parent 9b8ede9 commit 50811a2
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 48 deletions.
38 changes: 38 additions & 0 deletions rootfs/api/migrations/0015_auto_20160822_2103.py
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-22 21:03
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0014_appsettings_whitelist'),
]

operations = [
migrations.CreateModel(
name='TLS',
fields=[
('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('https_enforced', models.NullBooleanField(default=None)),
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
'get_latest_by': 'created',
},
),
migrations.AlterUniqueTogether(
name='tls',
unique_together=set([('app', 'uuid')]),
),
]
89 changes: 41 additions & 48 deletions rootfs/api/models/__init__.py
Expand Up @@ -114,75 +114,68 @@ class Meta:


from .app import App, validate_id_is_docker_compatible, validate_reserved_names, validate_app_structure # noqa
from .key import Key, validate_base64 # noqa
from .appsettings import AppSettings # noqa
from .build import Build # noqa
from .certificate import Certificate, validate_certificate # noqa
from .config import Config # noqa
from .domain import Domain # noqa
from .key import Key, validate_base64 # noqa
from .release import Release # noqa
from .config import Config # noqa
from .build import Build # noqa
from .appsettings import AppSettings # noqa
from .tls import TLS # noqa

# define update/delete callbacks for synchronizing
# models with the configuration management backend


def _log_build_created(**kwargs):
def _log_instance_created(**kwargs):
if kwargs.get('created'):
build = kwargs['instance']
# log only to the controller; this event will be logged in the release summary
build.app.log("build {} created".format(build))

instance = kwargs['instance']
message = '{} {} created'.format(instance.__class__.__name__, instance)
if hasattr(instance, 'app'):
instance.app.log(message)
else:
logger.info(message)

def _log_release_created(**kwargs):
if kwargs.get('created'):
release = kwargs['instance']
# append release lifecycle logs to the app
release.app.log(release.summary)

def _log_instance_updated(**kwargs):
instance = kwargs['instance']
message = '{} {} updated'.format(instance.__class__.__name__, instance)
if hasattr(instance, 'app'):
instance.app.log(message)
else:
logger.info(message)

def _log_config_updated(**kwargs):
config = kwargs['instance']
# log only to the controller; this event will be logged in the release summary
config.app.log("config {} updated".format(config))

def _log_instance_removed(**kwargs):
instance = kwargs['instance']
message = '{} {} removed'.format(instance.__class__.__name__, instance)
if hasattr(instance, 'app'):
instance.app.log(message)
else:
logger.info(message)

def _log_app_settings_updated(**kwargs):
appSettings = kwargs['instance']
# log only to the controller; this event will be logged in the release summary
appSettings.app.log("application settings {} updated".format(appSettings))


def _log_domain_added(**kwargs):
# special case: log the release summary
def _log_release_created(**kwargs):
if kwargs.get('created'):
domain = kwargs['instance']
domain.app.log("domain {} added".format(domain))


def _log_domain_removed(**kwargs):
domain = kwargs['instance']
domain.app.log("domain {} removed".format(domain))

release = kwargs['instance']
# append release lifecycle logs to the app
release.app.log(release.summary)

def _log_cert_added(**kwargs):
if kwargs.get('created'):
cert = kwargs['instance']
logger.info("cert {} added".format(cert))

# Log significant app-related events
post_save.connect(_log_release_created, sender=Release, dispatch_uid='api.models.log')

def _log_cert_removed(**kwargs):
cert = kwargs['instance']
logger.info("cert {} removed".format(cert))
post_save.connect(_log_instance_created, sender=Build, dispatch_uid='api.models.log')
post_save.connect(_log_instance_created, sender=Certificate, dispatch_uid='api.models.log')
post_save.connect(_log_instance_created, sender=Domain, dispatch_uid='api.models.log')

post_save.connect(_log_instance_updated, sender=AppSettings, dispatch_uid='api.models.log')
post_save.connect(_log_instance_updated, sender=Config, dispatch_uid='api.models.log')

# Log significant app-related events
post_save.connect(_log_build_created, sender=Build, dispatch_uid='api.models.log')
post_save.connect(_log_release_created, sender=Release, dispatch_uid='api.models.log')
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models.log')
post_save.connect(_log_domain_added, sender=Domain, dispatch_uid='api.models.log')
post_save.connect(_log_cert_added, sender=Certificate, dispatch_uid='api.models.log')
post_save.connect(_log_app_settings_updated, sender=AppSettings, dispatch_uid='api.models.log')
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
post_delete.connect(_log_cert_removed, sender=Certificate, dispatch_uid='api.models.log')
post_delete.connect(_log_instance_removed, sender=Certificate, dispatch_uid='api.models.log')
post_delete.connect(_log_instance_removed, sender=Domain, dispatch_uid='api.models.log')
post_delete.connect(_log_instance_removed, sender=TLS, dispatch_uid='api.models.log')


# automatically generate a new token on creation
Expand Down
53 changes: 53 additions & 0 deletions rootfs/api/models/tls.py
@@ -0,0 +1,53 @@
from django.db import models
from django.conf import settings

from api.exceptions import AlreadyExists
from api.models import UuidAuditedModel


class TLS(UuidAuditedModel):
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
app = models.ForeignKey('App', on_delete=models.CASCADE)
https_enforced = models.NullBooleanField(default=None)

class Meta:
get_latest_by = 'created'
unique_together = (('app', 'uuid'))
ordering = ['-created']

def __str__(self):
return "{}-{}".format(self.app.id, str(self.uuid)[:7])

def _check_previous_tls_settings(self):
try:
previous_tls_settings = self.app.tls_set.latest()

if (
previous_tls_settings.https_enforced is not None and
self.https_enforced == previous_tls_settings.https_enforced
):
self.delete()
raise AlreadyExists("{} changed nothing".format(self.owner))
except TLS.DoesNotExist:
pass

def save(self, *args, **kwargs):
self._check_previous_tls_settings()

app = str(self.app)
https_enforced = bool(self.https_enforced)

# get config for the service
config = self._load_service_config(app, 'router')

# See if the ssl.enforce annotation is available
if 'ssl.enforce' not in config:
config['ssl.enforce'] = 'false'

# convert from bool to string
config['ssl.enforce'] = str(https_enforced)

self._save_service_config(app, 'router', config)

# Save to DB
return super(TLS, self).save(*args, **kwargs)
12 changes: 12 additions & 0 deletions rootfs/api/serializers.py
Expand Up @@ -490,3 +490,15 @@ def validate_whitelist(self, data):
raise serializers.ValidationError(
"The address {} is not valid".format(address))
return data


class TLSSerializer(serializers.ModelSerializer):
"""Serialize a :class:`~api.models.TLS` model."""

app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all())
owner = serializers.ReadOnlyField(source='owner.username')

class Meta:
"""Metadata options for a :class:`AppSettingsSerializer`."""
model = models.TLS
fields = '__all__'
63 changes: 63 additions & 0 deletions rootfs/api/tests/test_tls.py
@@ -0,0 +1,63 @@
import requests_mock

from django.core.cache import cache
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

from api.models import App
from api.tests import adapter, DeisTransactionTestCase


@requests_mock.Mocker(real_http=True, adapter=adapter)
class TestTLS(DeisTransactionTestCase):
"""Tests setting and updating config values"""

fixtures = ['tests.json']

def setUp(self):
self.user = User.objects.get(username='autotest')
self.token = Token.objects.get(user=self.user).key
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)

def tearDown(self):
# make sure every test has a clean slate for k8s mocking
cache.clear()

def test_tls_enforced(self, mock_requests):
"""
Test that tls redirection can be enforced
"""
app_id = self.create_app()
app = App.objects.get(id=app_id)

data = {'https_enforced': True}
response = self.client.post(
'/v2/apps/{app_id}/tls'.format(**locals()),
data)
self.assertEqual(response.status_code, 201, response.data)
self.assertTrue(response.data.get('https_enforced'), response.data)
self.assertTrue(app.tls_set.latest().https_enforced)

data = {'https_enforced': False}
response = self.client.post(
'/v2/apps/{app_id}/tls'.format(**locals()),
data)
self.assertEqual(response.status_code, 201, response.data)
self.assertFalse(app.tls_set.latest().https_enforced)

# when the same data is sent again, a 409 is returned
conflict_response = self.client.post(
'/v2/apps/{app_id}/tls'.format(**locals()),
data)
self.assertEqual(conflict_response.status_code, 409, conflict_response.data)
self.assertFalse(app.tls_set.latest().https_enforced)
# also ensure that the previous tls UUID matches the latest,
# confirming this conflicting TLS object was deleted
self.assertEqual(response.data['uuid'], str(app.tls_set.latest().uuid))

# sending bad data returns a 400
data['https_enforced'] = "test"
response = self.client.post(
'/v2/apps/{app_id}/tls'.format(**locals()),
data)
self.assertEqual(response.status_code, 400, response.data)
3 changes: 3 additions & 0 deletions rootfs/api/urls.py
Expand Up @@ -63,6 +63,9 @@
# application ip whitelist
url(r"^apps/(?P<id>{})/whitelist/?".format(settings.APP_URL_REGEX),
views.WhitelistViewSet.as_view({'post': 'create', 'get': 'list', 'delete': 'delete'})),
# application TLS settings
url(r"^apps/(?P<id>{})/tls/?".format(settings.APP_URL_REGEX),
views.TLSViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
# apps sharing
url(r"^apps/(?P<id>{})/perms/(?P<username>[-_\w]+)/?".format(settings.APP_URL_REGEX),
views.AppPermsViewSet.as_view({'delete': 'destroy'})),
Expand Down
5 changes: 5 additions & 0 deletions rootfs/api/views.py
Expand Up @@ -406,6 +406,11 @@ def rollback(self, request, **kwargs):
return Response(response, status=status.HTTP_201_CREATED)


class TLSViewSet(AppResourceViewSet):
model = models.TLS
serializer_class = serializers.TLSSerializer


class BaseHookViewSet(BaseDeisViewSet):
permission_classes = [permissions.HasBuilderAuth]

Expand Down

0 comments on commit 50811a2

Please sign in to comment.