from django.contrib.auth.models import AbstractUser, Group from django.db import models from django.conf import settings from django.dispatch import receiver, Signal from django.utils.timezone import make_aware from rest_framework.authtoken.models import Token from django.db.models.signals import post_save, pre_delete # from django.db.models.signals import m2m_changed from datetime import datetime from .rabbitmq import RabbitMQInstance deployment_change = Signal(providing_args=['instance']) class User(AbstractUser): TYPE_CHOICES = ( ('apiclient', 'API-Client'), ('oidcuser', 'OIDC User'), ('admin', 'Admin'), ) user_type = models.CharField( max_length=20, choices=TYPE_CHOICES, default='oidcuser', ) sub = models.CharField(max_length=150, blank=True, null=True) password = models.CharField(max_length=150, blank=True, null=True) # we hide deleted keys here # the full list of ssh keys is at self._ssh_keys @property def ssh_keys(self): return self._ssh_keys.filter(deleted=False) @receiver(post_save, sender=settings.AUTH_USER_MODEL) def create_auth_token(sender, instance=None, created=False, **kwargs): if instance.user_type == 'apiclient' and created: Token.objects.create(user=instance) def construct_user(user_info): return User( sub=user_info['sub'], name=user_info['name'], first_name=user_info['given_name'], last_name=user_info['family_name'], email=user_info['email'], username=user_info['email'], ) class Site(models.Model): client = models.OneToOneField( User, related_name='site', ) name = models.CharField(max_length=150, unique=True) description = models.TextField(max_length=300, blank=True) last_fetch = models.DateTimeField( default=make_aware(datetime.utcfromtimestamp(0)), editable=False) def __str__(self): return self.name def client_updated(self): self.last_fetch = make_aware(datetime.now()) self.save() def clientapi_get_deployments(self, all=False): services = {} for service in self.services.all(): deployments = ( service.deployments .filter(user__user_type='oidcuser') # we do not exclude deployments without ssh_keys, as the # ssh_keys_to_withdraw still need to be propagated # .exclude(ssh_keys=None) ) if not all: deployments = deployments.filter( last_change__gt=self.last_fetch) # deployments for this site services[service.name] = deployments for deployment in deployments.all(): # TODO replace this optimism with an acknowledgement deployment.client_updated() # TODO we expect the client to get the update here self.client_updated() return services @receiver(post_save, sender=Site) def register_at_rabbitmq( sender, instance=None, created=False, **kwargs): if not created: return RabbitMQInstance().register_site(instance) @receiver(pre_delete, sender=Site) def deregister_at_rabbitmq( sender, instance=None, **kwargs): RabbitMQInstance().deregister_site(instance) class Service(models.Model): name = models.CharField(max_length=150, unique=True) description = models.TextField(max_length=300, blank=True) site = models.ForeignKey( Site, related_name='services') groups = models.ManyToManyField( Group, related_name='services', blank=True) def __str__(self): return self.name + '@' + self.site.name class SSHPublicKey(models.Model): name = models.CharField(max_length=150, unique=True) key = models.TextField(max_length=1000) # hidden field at the user user = models.ForeignKey( User, related_name='_ssh_keys') # has the user triggered the deletion of this key deleted = models.BooleanField( default=False, editable=False, ) # does not directly delete the key if the key is deployed or withdrawn # somewhere # the receiver 'delete_withdrawn_ssh_key' does the actual deletion def delete_key(self): if (not self.deployments.exists() and not self.withdrawn_deployments.exists()): self.delete() return self.deleted = True self.save() # delete implies withdrawing the key from all clients for deployment in self.deployments.all(): deployment.withdraw_key(self) # when a key is withdrawn by a client we try to finally delete it def try_final_deletion(self): if (self.deleted and not self.deployments.exists() and not self.withdrawn_deployments.exists()): self.delete() return def __str__(self): if self.deleted: return "DELETED: {}".format(self.name) return self.name class Deployment(models.Model): user = models.ForeignKey( User, related_name='deployments', on_delete=models.CASCADE, ) service = models.ForeignKey( Service, related_name='deployments', on_delete=models.CASCADE, ) # SET_NULL: we allow credentials to be deleted after deployment ssh_keys = models.ManyToManyField( SSHPublicKey, related_name='deployments', blank=True, ) # these ssh keys are to be withdrawn by the clients ssh_keys_to_withdraw = models.ManyToManyField( SSHPublicKey, related_name='withdrawn_deployments', blank=True, ) last_change = models.DateTimeField( auto_now=True ) def __str__(self): return '{}@{}'.format(self.user, self.service) def deploy_key(self, key): # key state: -> (2.5) self.ssh_keys.add(key) if key in self.ssh_keys_to_withdraw.all(): self.ssh_keys_to_withdraw.remove(key) self.save() self.send_change() def withdraw_key(self, key): # key state: -> (4) self.ssh_keys.remove(key) # keys which are to be withdrawn by the clients self.ssh_keys_to_withdraw.add(key) self.save() self.send_change() def client_updated(self): withdrawn_keys = list(self.ssh_keys_to_withdraw.all()) # the client has withdrawn the keys so we can empty the list self.ssh_keys_to_withdraw.clear() for key in withdrawn_keys: key.try_final_deletion() self.save() def send_change(self): deployment_change.send(sender=self.__class__, instance=self)