66
77from __future__ import unicode_literals
88import base64
9+ from datetime import datetime
910import etcd
1011import importlib
1112import logging
1718
1819from django .conf import settings
1920from django .contrib .auth import get_user_model
20- from django .core .exceptions import ValidationError
21+ from django .core .exceptions import ValidationError , SuspiciousOperation
2122from django .db import models
2223from django .db .models import Count
2324from django .db .models import Max
2627from django .utils .encoding import python_2_unicode_compatible
2728from docker .utils import utils as dockerutils
2829from json_field .fields import JSONField
30+ from OpenSSL import crypto
2931import requests
3032from 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+
113125class 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
829874class 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+
881936def _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+
920988def _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):
9431011post_save .connect (_log_release_created , sender = Release , dispatch_uid = 'api.models.log' )
9441012post_save .connect (_log_config_updated , sender = Config , dispatch_uid = 'api.models.log' )
9451013post_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' )
9461015post_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' )
0 commit comments