Skip to content

Commit d8bf7d3

Browse files
author
Matthew Fisher
committed
feat(controller): add deis certs
1 parent 7fd7a4d commit d8bf7d3

7 files changed

Lines changed: 409 additions & 2 deletions

File tree

api/models.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import unicode_literals
88
import base64
9+
from datetime import datetime
910
import etcd
1011
import importlib
1112
import logging
@@ -17,7 +18,7 @@
1718

1819
from django.conf import settings
1920
from django.contrib.auth import get_user_model
20-
from django.core.exceptions import ValidationError
21+
from django.core.exceptions import ValidationError, SuspiciousOperation
2122
from django.db import models
2223
from django.db.models import Count
2324
from django.db.models import Max
@@ -26,6 +27,7 @@
2627
from django.utils.encoding import python_2_unicode_compatible
2728
from docker.utils import utils as dockerutils
2829
from json_field.fields import JSONField
30+
from OpenSSL import crypto
2931
import requests
3032
from rest_framework.authtoken.models import Token
3133

@@ -110,6 +112,16 @@ def validate_domain(value):
110112
raise ValidationError('"{}" contains unexpected characters'.format(value))
111113

112114

115+
def validate_domain_certificate(value):
116+
try:
117+
cert = crypto.load_certificate(crypto.FILETYPE_PEM, value)
118+
Domain.objects.get(domain=cert.get_subject().CN)
119+
except crypto.Error as e:
120+
raise ValidationError('Could not load certificate: {}'.format(e))
121+
except Domain.DoesNotExist:
122+
raise ValidationError('No matching domain was found for {}'.format(cert.get_subject().CN))
123+
124+
113125
class AuditedModel(models.Model):
114126
"""Add created and updated fields to a model."""
115127

@@ -820,11 +832,44 @@ class Domain(AuditedModel):
820832
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
821833
app = models.ForeignKey('App')
822834
domain = models.TextField(blank=False, null=False, unique=True)
835+
cert = models.ForeignKey('DomainCert', null=True)
823836

824837
def __str__(self):
825838
return self.domain
826839

827840

841+
@python_2_unicode_compatible
842+
class DomainCert(AuditedModel):
843+
"""
844+
Public and private key pair used to secure application traffic at the router.
845+
"""
846+
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
847+
# there is no upper limit on the size of an x.509 certificate
848+
certificate = models.TextField(validators=[validate_domain_certificate])
849+
key = models.TextField()
850+
# X.509 certificates allow any string of information as the common name.
851+
common_name = models.TextField(unique=True)
852+
expires = models.DateTimeField()
853+
854+
def __str__(self):
855+
return self.common_name
856+
857+
def _get_certificate(self):
858+
try:
859+
return crypto.load_certificate(crypto.FILETYPE_PEM, self.certificate)
860+
except crypto.Error as e:
861+
raise SuspiciousOperation(e)
862+
863+
def save(self, *args, **kwargs):
864+
certificate = self._get_certificate()
865+
if not self.common_name:
866+
self.common_name = certificate.get_subject().CN
867+
if not self.expires:
868+
# convert openssl's expiry date format to Django's DateTimeField format
869+
self.expires = datetime.strptime(certificate.get_notAfter(), '%Y%m%d%H%M%SZ')
870+
return super(DomainCert, self).save(*args, **kwargs)
871+
872+
828873
@python_2_unicode_compatible
829874
class Key(UuidAuditedModel):
830875
"""An SSH public key."""
@@ -878,6 +923,16 @@ def _log_domain_removed(**kwargs):
878923
log_event(domain.app, msg)
879924

880925

926+
def _log_cert_added(**kwargs):
927+
cert = kwargs['instance']
928+
logger.info("cert {} added".format(cert))
929+
930+
931+
def _log_cert_removed(**kwargs):
932+
cert = kwargs['instance']
933+
logger.info("cert {} removed".format(cert))
934+
935+
881936
def _etcd_publish_key(**kwargs):
882937
key = kwargs['instance']
883938
_etcd_client.write('/deis/builder/users/{}/{}'.format(
@@ -917,6 +972,19 @@ def _etcd_purge_app(**kwargs):
917972
pass
918973

919974

975+
def _etcd_publish_cert(**kwargs):
976+
cert = kwargs['instance']
977+
if kwargs['created']:
978+
_etcd_client.write('/deis/certs/{}/cert'.format(cert), cert.certificate)
979+
_etcd_client.write('/deis/certs/{}/key'.format(cert), cert.key)
980+
981+
982+
def _etcd_purge_cert(**kwargs):
983+
cert = kwargs['instance']
984+
_etcd_client.delete('/deis/certs/{}'.format(cert),
985+
prevExist=True, dir=True, recursive=True)
986+
987+
920988
def _etcd_publish_domains(**kwargs):
921989
app = kwargs['instance'].app
922990
app_domains = app.domain_set.all()
@@ -943,7 +1011,9 @@ def _etcd_purge_domains(**kwargs):
9431011
post_save.connect(_log_release_created, sender=Release, dispatch_uid='api.models.log')
9441012
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models.log')
9451013
post_save.connect(_log_domain_added, sender=Domain, dispatch_uid='api.models.log')
1014+
post_save.connect(_log_cert_added, sender=DomainCert, dispatch_uid='api.models.log')
9461015
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
1016+
post_delete.connect(_log_cert_removed, sender=DomainCert, dispatch_uid='api.models.log')
9471017

9481018

9491019
# automatically generate a new token on creation
@@ -968,3 +1038,5 @@ def create_auth_token(sender, instance=None, created=False, **kwargs):
9681038
post_delete.connect(_etcd_purge_domains, sender=Domain, dispatch_uid='api.models')
9691039
post_save.connect(_etcd_create_app, sender=App, dispatch_uid='api.models')
9701040
post_delete.connect(_etcd_purge_app, sender=App, dispatch_uid='api.models')
1041+
post_save.connect(_etcd_publish_cert, sender=DomainCert, dispatch_uid='api.models')
1042+
post_delete.connect(_etcd_purge_cert, sender=DomainCert, dispatch_uid='api.models')

api/serializers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,21 @@ def validate_domain(self, value):
260260
return value
261261

262262

263+
class DomainCertSerializer(ModelSerializer):
264+
"""Serialize a :class:`~api.models.Cert` model."""
265+
266+
expires = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True)
267+
created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True)
268+
updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True)
269+
270+
class Meta:
271+
"""Metadata options for a DomainCertSerializer."""
272+
model = models.DomainCert
273+
extra_kwargs = {'certificate': {'write_only': True},
274+
'key': {'write_only': True}}
275+
read_only_fields = ['owner', 'common_name', 'expires', 'created', 'updated']
276+
277+
263278
class PushSerializer(ModelSerializer):
264279
"""Serialize a :class:`~api.models.Push` model."""
265280

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# -*- coding: utf-8 -*-
2+
from south.utils import datetime_utils as datetime
3+
from south.db import db
4+
from south.v2 import SchemaMigration
5+
from django.db import models
6+
7+
8+
class Migration(SchemaMigration):
9+
10+
def forwards(self, orm):
11+
# Adding model 'DomainCert'
12+
db.create_table(u'api_domaincert', (
13+
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14+
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
15+
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
16+
('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
17+
('certificate', self.gf('django.db.models.fields.TextField')()),
18+
('key', self.gf('django.db.models.fields.TextField')()),
19+
('common_name', self.gf('django.db.models.fields.TextField')(unique=True)),
20+
('expires', self.gf('django.db.models.fields.DateTimeField')()),
21+
))
22+
db.send_create_signal(u'api', ['DomainCert'])
23+
24+
# Adding field 'Domain.cert'
25+
db.add_column(u'api_domain', 'cert',
26+
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api.DomainCert'], null=True),
27+
keep_default=False)
28+
29+
30+
def backwards(self, orm):
31+
# Deleting model 'DomainCert'
32+
db.delete_table(u'api_domaincert')
33+
34+
# Deleting field 'Domain.cert'
35+
db.delete_column(u'api_domain', 'cert_id')
36+
37+
38+
models = {
39+
u'api.app': {
40+
'Meta': {'object_name': 'App'},
41+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
42+
'id': ('django.db.models.fields.SlugField', [], {'default': "'dogged-quotient'", 'unique': 'True', 'max_length': '64'}),
43+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
44+
'structure': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
45+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
46+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
47+
},
48+
u'api.build': {
49+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Build'},
50+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
51+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
52+
'dockerfile': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
53+
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
54+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
55+
'procfile': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
56+
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
57+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
58+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
59+
},
60+
u'api.config': {
61+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Config'},
62+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
63+
'cpu': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
64+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
65+
'memory': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
66+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
67+
'tags': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
68+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
69+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
70+
'values': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'})
71+
},
72+
u'api.container': {
73+
'Meta': {'ordering': "[u'created']", 'object_name': 'Container'},
74+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
75+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
76+
'num': ('django.db.models.fields.PositiveIntegerField', [], {}),
77+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
78+
'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Release']"}),
79+
'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
80+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
81+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
82+
},
83+
u'api.domain': {
84+
'Meta': {'object_name': 'Domain'},
85+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
86+
'cert': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.DomainCert']", 'null': 'True'}),
87+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
88+
'domain': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
89+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
90+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
91+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
92+
},
93+
u'api.domaincert': {
94+
'Meta': {'object_name': 'DomainCert'},
95+
'certificate': ('django.db.models.fields.TextField', [], {}),
96+
'common_name': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
97+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
98+
'expires': ('django.db.models.fields.DateTimeField', [], {}),
99+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
100+
'key': ('django.db.models.fields.TextField', [], {}),
101+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
102+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
103+
},
104+
u'api.key': {
105+
'Meta': {'unique_together': "((u'owner', u'id'),)", 'object_name': 'Key'},
106+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
107+
'id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
108+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
109+
'public': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
110+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
111+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
112+
},
113+
u'api.push': {
114+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Push'},
115+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
116+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
117+
'fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
118+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
119+
'receive_repo': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
120+
'receive_user': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
121+
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
122+
'ssh_connection': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
123+
'ssh_original_command': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
124+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
125+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
126+
},
127+
u'api.release': {
128+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'version'),)", 'object_name': 'Release'},
129+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
130+
'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Build']", 'null': 'True'}),
131+
'config': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Config']"}),
132+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
133+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
134+
'summary': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
135+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
136+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
137+
'version': ('django.db.models.fields.PositiveIntegerField', [], {})
138+
},
139+
u'auth.group': {
140+
'Meta': {'object_name': 'Group'},
141+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
142+
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
143+
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
144+
},
145+
u'auth.permission': {
146+
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
147+
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
148+
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
149+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
150+
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
151+
},
152+
u'auth.user': {
153+
'Meta': {'object_name': 'User'},
154+
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
155+
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
156+
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
157+
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
158+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
159+
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
160+
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
161+
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
162+
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
163+
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
164+
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
165+
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
166+
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
167+
},
168+
u'contenttypes.contenttype': {
169+
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
170+
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
171+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172+
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
173+
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
174+
}
175+
}
176+
177+
complete_apps = ['api']

api/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
5252
from .test_build import * # noqa
5353
from .test_config import * # noqa
5454
from .test_domain import * # noqa
55+
from .test_domain_cert import * # noqa
5556
from .test_container import * # noqa
5657
from .test_hooks import * # noqa
5758
from .test_key import * # noqa

0 commit comments

Comments
 (0)