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,13 @@ def validate_domain(value):
110112 raise ValidationError ('"{}" contains unexpected characters' .format (value ))
111113
112114
115+ def validate_certificate (value ):
116+ try :
117+ crypto .load_certificate (crypto .FILETYPE_PEM , value )
118+ except crypto .Error as e :
119+ raise ValidationError ('Could not load certificate: {}' .format (e ))
120+
121+
113122class AuditedModel (models .Model ):
114123 """Add created and updated fields to a model."""
115124
@@ -825,6 +834,38 @@ def __str__(self):
825834 return self .domain
826835
827836
837+ @python_2_unicode_compatible
838+ class Certificate (AuditedModel ):
839+ """
840+ Public and private key pair used to secure application traffic at the router.
841+ """
842+ owner = models .ForeignKey (settings .AUTH_USER_MODEL )
843+ # there is no upper limit on the size of an x.509 certificate
844+ certificate = models .TextField (validators = [validate_certificate ])
845+ key = models .TextField ()
846+ # X.509 certificates allow any string of information as the common name.
847+ common_name = models .TextField (unique = True )
848+ expires = models .DateTimeField ()
849+
850+ def __str__ (self ):
851+ return self .common_name
852+
853+ def _get_certificate (self ):
854+ try :
855+ return crypto .load_certificate (crypto .FILETYPE_PEM , self .certificate )
856+ except crypto .Error as e :
857+ raise SuspiciousOperation (e )
858+
859+ def save (self , * args , ** kwargs ):
860+ certificate = self ._get_certificate ()
861+ if not self .common_name :
862+ self .common_name = certificate .get_subject ().CN
863+ if not self .expires :
864+ # convert openssl's expiry date format to Django's DateTimeField format
865+ self .expires = datetime .strptime (certificate .get_notAfter (), '%Y%m%d%H%M%SZ' )
866+ return super (Certificate , self ).save (* args , ** kwargs )
867+
868+
828869@python_2_unicode_compatible
829870class Key (UuidAuditedModel ):
830871 """An SSH public key."""
@@ -878,6 +919,16 @@ def _log_domain_removed(**kwargs):
878919 log_event (domain .app , msg )
879920
880921
922+ def _log_cert_added (** kwargs ):
923+ cert = kwargs ['instance' ]
924+ logger .info ("cert {} added" .format (cert ))
925+
926+
927+ def _log_cert_removed (** kwargs ):
928+ cert = kwargs ['instance' ]
929+ logger .info ("cert {} removed" .format (cert ))
930+
931+
881932def _etcd_publish_key (** kwargs ):
882933 key = kwargs ['instance' ]
883934 _etcd_client .write ('/deis/builder/users/{}/{}' .format (
@@ -917,33 +968,45 @@ def _etcd_purge_app(**kwargs):
917968 pass
918969
919970
971+ def _etcd_publish_cert (** kwargs ):
972+ cert = kwargs ['instance' ]
973+ if kwargs ['created' ]:
974+ _etcd_client .write ('/deis/certs/{}/cert' .format (cert ), cert .certificate )
975+ _etcd_client .write ('/deis/certs/{}/key' .format (cert ), cert .key )
976+
977+
978+ def _etcd_purge_cert (** kwargs ):
979+ cert = kwargs ['instance' ]
980+ try :
981+ _etcd_client .delete ('/deis/certs/{}' .format (cert ),
982+ prevExist = True , dir = True , recursive = True )
983+ except KeyError :
984+ pass
985+
986+
920987def _etcd_publish_domains (** kwargs ):
921- app = kwargs ['instance' ].app
922- app_domains = app .domain_set .all ()
923- if app_domains :
924- _etcd_client .write ('/deis/domains/{}' .format (app ),
925- ' ' .join (str (d .domain ) for d in app_domains ))
988+ domain = kwargs ['instance' ]
989+ if kwargs ['created' ]:
990+ _etcd_client .write ('/deis/domains/{}' .format (domain ), domain .app )
926991
927992
928993def _etcd_purge_domains (** kwargs ):
929- app = kwargs ['instance' ].app
930- app_domains = app .domain_set .all ()
931- if app_domains :
932- _etcd_client .write ('/deis/domains/{}' .format (app ),
933- ' ' .join (str (d .domain ) for d in app_domains ))
934- else :
935- try :
936- _etcd_client .delete ('/deis/domains/{}' .format (app ))
937- except KeyError :
938- pass
994+ domain = kwargs ['instance' ]
995+ try :
996+ _etcd_client .delete ('/deis/certs/{}' .format (domain ),
997+ prevExist = True , dir = True , recursive = True )
998+ except KeyError :
999+ pass
9391000
9401001
9411002# Log significant app-related events
9421003post_save .connect (_log_build_created , sender = Build , dispatch_uid = 'api.models.log' )
9431004post_save .connect (_log_release_created , sender = Release , dispatch_uid = 'api.models.log' )
9441005post_save .connect (_log_config_updated , sender = Config , dispatch_uid = 'api.models.log' )
9451006post_save .connect (_log_domain_added , sender = Domain , dispatch_uid = 'api.models.log' )
1007+ post_save .connect (_log_cert_added , sender = Certificate , dispatch_uid = 'api.models.log' )
9461008post_delete .connect (_log_domain_removed , sender = Domain , dispatch_uid = 'api.models.log' )
1009+ post_delete .connect (_log_cert_removed , sender = Certificate , dispatch_uid = 'api.models.log' )
9471010
9481011
9491012# automatically generate a new token on creation
@@ -968,3 +1031,5 @@ def create_auth_token(sender, instance=None, created=False, **kwargs):
9681031 post_delete .connect (_etcd_purge_domains , sender = Domain , dispatch_uid = 'api.models' )
9691032 post_save .connect (_etcd_create_app , sender = App , dispatch_uid = 'api.models' )
9701033 post_delete .connect (_etcd_purge_app , sender = App , dispatch_uid = 'api.models' )
1034+ post_save .connect (_etcd_publish_cert , sender = Certificate , dispatch_uid = 'api.models' )
1035+ post_delete .connect (_etcd_purge_cert , sender = Certificate , dispatch_uid = 'api.models' )
0 commit comments