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 rest_framework.authtoken.models import Token from django.db.models.signals import post_save, m2m_changed from datetime import datetime import requests from requests.auth import HTTPBasicAuth 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=datetime.utcfromtimestamp(0)) def __str__(self): return self.name 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_got_deployment() self.last_fetch = datetime.now() self.save() return services @receiver(post_save, sender=Site) def register_client_at_rabbitmq(sender, instance=None, created=False, **kwargs): if not created: return print('registerring client {}'.format(instance.client.username)) # %2f: url encoded '/' as this is the vhost we use api = 'http://localhost:15672/api' username = instance.client.username vhost = '%2f' exchange = 'deployments' # guest only works on the localhost auth = HTTPBasicAuth('guest', 'guest') # create user user_creation_uri = '{}/users/{}/'.format( api, username ) user_creation_data = { 'password': str(instance.client.auth_token.key), 'tags': '', } r = requests.put(user_creation_uri, json=user_creation_data, auth=auth) print('status_code {} {}'.format(r.status_code, r.text)) # set permissions for the user set_permission_uri = '{}/permissions/{}/{}/'.format( api, vhost, username, ) permission = '^(amq\.gen.*|{})'.format(exchange) set_permission_data = { 'configure': permission, 'write': permission, 'read': permission, } set_topic_permission_uri = '{}/topic-permissions/{}/{}/'.format( api, vhost, username, ) r = requests.put(set_permission_uri, json=set_permission_data, auth=auth) print('status_code {} {}'.format(r.status_code, r.text)) # set permissions for the correct topics # we construct a regex to match the services of the site services = '' omitBar = True for service in instance.services.all(): prefix = '|' if omitBar: prefix = '' omitBar = False services = services + prefix + service.name set_topic_permission_data = { 'exchange': exchange, 'write': '^$', 'read': '^service\.({})$'.format(services), } r = requests.put( set_topic_permission_uri, json=set_topic_permission_data, auth=auth) print('status_code {} {}'.format(r.status_code, r.text)) 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() for deployment in self.deployments.all(): deployment.withdraw_key(self) def __str__(self): if self.deleted: return "DELETED: {}".format(self.name) return self.name # finally delete the key if all the deployments are withdrawn # and the withdrawal was seen by all clients @receiver(m2m_changed, sender=SSHPublicKey) def delete_withdrawn_ssh_key( sender, instance=None, created=False, **kwargs): if (instance.deleted and not instance.deployments.exists() and not instance.withdrawn_deployments.exists()): instance.delete() 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 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_got_deployment(self): # the client has withdrawn the keys so we can empty the list self.ssh_keys_to_withdraw.clear() # TODO: does the deletion of the ssh_keys actually occur? self.save() def send_change(self): deployment_change.send(sender=self.__class__, instance=self) def __str__(self): return '{}@{}'.format(self.user, self.service) @receiver(post_save, sender=Deployment) def publish_deployment_creation( sender, instance=None, created=False, **kwargs): instance.send_change()