models.py 7.33 KB
Newer Older
Lukas Burgey's avatar
Lukas Burgey committed
1
2
from django.contrib.auth.models import AbstractUser, Group
from django.db import models
3
from django.conf import settings
4
from django.dispatch import receiver, Signal
Lukas Burgey's avatar
Lukas Burgey committed
5
from django.utils.timezone import make_aware
6
from rest_framework.authtoken.models import Token
Lukas Burgey's avatar
Lukas Burgey committed
7
8
from django.db.models.signals import post_save
# from django.db.models.signals import m2m_changed
Lukas Burgey's avatar
Lukas Burgey committed
9
from datetime import datetime
Lukas Burgey's avatar
Lukas Burgey committed
10

Lukas Burgey's avatar
Lukas Burgey committed
11
from .rabbitmq import RabbitMQInstance
12

13
14
deployment_change = Signal(providing_args=['instance'])

Lukas Burgey's avatar
Lukas Burgey committed
15
16

class User(AbstractUser):
17
18
19
20
21
22
23
24
25
26
    TYPE_CHOICES = (
            ('apiclient', 'API-Client'),
            ('oidcuser', 'OIDC User'),
            ('admin', 'Admin'),
            )
    user_type = models.CharField(
            max_length=20,
            choices=TYPE_CHOICES,
            default='oidcuser',
            )
Lukas Burgey's avatar
Lukas Burgey committed
27
28
29
    sub = models.CharField(max_length=150, blank=True, null=True)
    password = models.CharField(max_length=150, blank=True, null=True)

Lukas Burgey's avatar
Lukas Burgey committed
30
31
32
33
34
35
    # 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)

Lukas Burgey's avatar
Lukas Burgey committed
36

37
38
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
39
40
    if instance.user_type == 'apiclient' and created:
        Token.objects.create(user=instance)
41
42


Lukas Burgey's avatar
Lukas Burgey committed
43
44
45
46
47
48
49
50
51
52
53
54
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):
55
56
57
    client = models.OneToOneField(
            User,
            related_name='site',
58
            )
Lukas Burgey's avatar
Lukas Burgey committed
59
60
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)
Lukas Burgey's avatar
Lukas Burgey committed
61
    last_fetch = models.DateTimeField(default=datetime.utcfromtimestamp(0))
Lukas Burgey's avatar
Lukas Burgey committed
62
63
64
65

    def __str__(self):
        return self.name

Lukas Burgey's avatar
Lukas Burgey committed
66
    def client_updated(self):
Lukas Burgey's avatar
Lukas Burgey committed
67
68
69
        self.last_fetch = make_aware(datetime.now())
        self.save()

70
    def clientapi_get_deployments(self, all=False):
Lukas Burgey's avatar
Lukas Burgey committed
71
72
73
        services = {}

        for service in self.services.all():
74
            deployments = (
75
76
77
78
79
80
                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)
                )
81
            if not all:
82
83
                deployments = deployments.filter(
                        last_change__gt=self.last_fetch)
84

85
            # deployments for this site
86
87
88
89
            services[service.name] = deployments

            for deployment in deployments.all():
                # TODO replace this optimism with an acknowledgement
Lukas Burgey's avatar
Lukas Burgey committed
90
                deployment.client_updated()
91

Lukas Burgey's avatar
Lukas Burgey committed
92
93
        # TODO we expect the client to get the update here
        self.client_updated()
Lukas Burgey's avatar
Lukas Burgey committed
94
95
        return services

Lukas Burgey's avatar
Lukas Burgey committed
96

97
@receiver(post_save, sender=Site)
Lukas Burgey's avatar
Lukas Burgey committed
98
99
def register_at_rabbitmq(
        sender, instance=None, created=False, **kwargs):
100
101
102
    if not created:
        return

Lukas Burgey's avatar
Lukas Burgey committed
103
    RabbitMQInstance().register_site(instance)
104
105


Lukas Burgey's avatar
Lukas Burgey committed
106
107
108
109
class Service(models.Model):
    name = models.CharField(max_length=150, unique=True)
    description = models.TextField(max_length=300, blank=True)
    site = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
110
111
            Site,
            related_name='services')
Lukas Burgey's avatar
Lukas Burgey committed
112
    groups = models.ManyToManyField(
Lukas Burgey's avatar
Lukas Burgey committed
113
114
115
            Group,
            related_name='services',
            blank=True)
Lukas Burgey's avatar
Lukas Burgey committed
116
117
118
119
120
121
122
123

    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)
Lukas Burgey's avatar
Lukas Burgey committed
124
    # hidden field at the user
Lukas Burgey's avatar
Lukas Burgey committed
125
    user = models.ForeignKey(
Lukas Burgey's avatar
Lukas Burgey committed
126
            User,
Lukas Burgey's avatar
Lukas Burgey committed
127
            related_name='_ssh_keys')
Lukas Burgey's avatar
Lukas Burgey committed
128

Lukas Burgey's avatar
Lukas Burgey committed
129
130
131
132
133
    # has the user triggered the deletion of this key
    deleted = models.BooleanField(
            default=False,
            editable=False,
            )
134

135
136
137
    # does not directly delete the key if the key is deployed or withdrawn
    # somewhere
    # the receiver 'delete_withdrawn_ssh_key' does the actual deletion
138
    def delete_key(self):
139
140
141
142
143
        if (not self.deployments.exists()
                and not self.withdrawn_deployments.exists()):
            self.delete()
            return

144
145
146
        self.deleted = True
        self.save()

Lukas Burgey's avatar
Lukas Burgey committed
147
        # delete implies withdrawing the key from all clients
148
149
150
        for deployment in self.deployments.all():
            deployment.withdraw_key(self)

Lukas Burgey's avatar
Lukas Burgey committed
151
152
153
154
155
156
157
158
    # 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

Lukas Burgey's avatar
Lukas Burgey committed
159
    def __str__(self):
Lukas Burgey's avatar
Lukas Burgey committed
160
161
        if self.deleted:
            return "DELETED: {}".format(self.name)
Lukas Burgey's avatar
Lukas Burgey committed
162
163
164
        return self.name


165
166
# finally delete the key if all the deployments are withdrawn
# and the withdrawal was seen by all clients
Lukas Burgey's avatar
Lukas Burgey committed
167
168
169
# @receiver(m2m_changed, sender=SSHPublicKey)
# def delete_withdrawn_ssh_key(
#         sender, instance=None, created=False, **kwargs):
Lukas Burgey's avatar
Lukas Burgey committed
170
#
Lukas Burgey's avatar
Lukas Burgey committed
171
172
173
174
175
#     if (instance.deleted
#             and not instance.deployments.exists()
#             and not instance.withdrawn_deployments.exists()):
#         # TODO this does not work (see the m2m_changed signal doc)
#         instance.delete()
176
177


Lukas Burgey's avatar
Lukas Burgey committed
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
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,
            )
195
196
197
198
199
200
201
202

    # these ssh keys are to be withdrawn by the clients
    ssh_keys_to_withdraw = models.ManyToManyField(
            SSHPublicKey,
            related_name='withdrawn_deployments',
            blank=True,
            )

203
204
205
    last_change = models.DateTimeField(
            auto_now=True
            )
Lukas Burgey's avatar
Lukas Burgey committed
206

Lukas Burgey's avatar
Lukas Burgey committed
207
208
209
    def __str__(self):
        return '{}@{}'.format(self.user, self.service)

210
211
212
213
214
215
216
    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()
217
        self.send_change()
218
219
220
221
222
223
224
225

    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()
226
        self.send_change()
227

Lukas Burgey's avatar
Lukas Burgey committed
228
229
230
    def client_updated(self):
        withdrawn_keys = list(self.ssh_keys_to_withdraw.all())

231
232
        # the client has withdrawn the keys so we can empty the list
        self.ssh_keys_to_withdraw.clear()
Lukas Burgey's avatar
Lukas Burgey committed
233
234
235
236

        for key in withdrawn_keys:
            key.try_final_deletion()

237
238
        self.save()

239
240
241
242
    def send_change(self):
        deployment_change.send(sender=self.__class__, instance=self)


Lukas Burgey's avatar
Lukas Burgey committed
243
244
245
246
# @receiver(post_save, sender=Deployment)
# def publish_deployment_creation(
#         sender, instance=None, created=False, **kwargs):
#     instance.send_change()