-
Notifications
You must be signed in to change notification settings - Fork 63
/
models.py
349 lines (275 loc) · 12.8 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
import time
from django.db import models
from django.db import connection
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, \
Permission, Group
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.dispatch import Signal
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from ..compat import TenantMixin
from ..compat import get_public_schema_name, get_tenant_model
from ..permissions.models import UserTenantPermissions, \
PermissionsMixinFacade
# An existing user removed from a tenant
tenant_user_removed = Signal(providing_args=["user", "tenant"])
# An existing user added to a tenant
tenant_user_added = Signal(providing_args=["user", "tenant"])
# A new user is created
tenant_user_created = Signal(providing_args=["user"])
# An existing user is deleted
tenant_user_deleted = Signal(providing_args=["user"])
class InactiveError(Exception):
pass
class ExistsError(Exception):
pass
class DeleteError(Exception):
pass
class SchemaError(Exception):
pass
def schema_required(func):
def inner(self, *args, **options):
tenant_schema = self.schema_name
# Save current schema and restore it when we're done
saved_schema = connection.get_schema()
# Set schema to this tenants schema to start building permissions in that tenant
connection.set_schema(tenant_schema)
try:
result = func(self, *args, **options)
finally:
# Even if an exception is raised we need to reset our schema state
connection.set_schema(saved_schema)
return result
return inner
class TenantBase(TenantMixin):
"""
Contains global data and settings for the tenant model.
"""
slug = models.SlugField(_('Tenant URL Name'), blank=True)
# The owner of the tenant. Only they can delete it. This can be changed, but it
# can't be blank. There should always be an owner.
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created = models.DateTimeField()
modified = models.DateTimeField(blank=True)
# Schema will be automatically created and synced when it is saved
auto_create_schema = True
# Schema will be automatically deleted when related tenant is deleted
auto_drop_schema = True
def save(self, *args, **kwargs):
if not self.pk:
self.created = timezone.now()
self.modified = timezone.now()
super(TenantBase, self).save(*args, **kwargs)
def delete(self, force_drop=False):
if force_drop:
super(TenantBase, self).delete(force_drop=True)
else:
raise DeleteError("Not supported -- delete_tenant() should be used.")
@schema_required
def add_user(self, user_obj, is_superuser=False, is_staff=False):
# User already is linked here..
if self.user_set.filter(id=user_obj.id).exists():
raise ExistsError("User already added to tenant: %s" % user_obj)
# User not linked to this tenant, so we need to create tenant permissions
user_tenant_perms = UserTenantPermissions.objects.create(
profile=user_obj,
is_staff=is_staff,
is_superuser=is_superuser
)
# Link user to tenant
user_obj.tenants.add(self)
tenant_user_added.send(sender=self.__class__, user=user_obj, tenant=self)
@schema_required
def remove_user(self, user_obj):
# Test that user is already in the tenant
self.user_set.get(id=user_obj.id)
if not user_obj.is_active:
raise InactiveError("User specified is not an active user: %s" % user_obj)
# Dont allow removing an owner from a tenant. This must be done
# Through delete tenant or transfer_ownership
if user_obj.id == self.owner.id:
raise DeleteError("Cannot remove owner from tenant: %s" % self.owner)
user_tenant_perms = user_obj.usertenantpermissions
# Remove all current groups from user..
groups = user_tenant_perms.groups
groups.clear()
# Unlink from tenant
UserTenantPermissions.objects.filter(id=user_tenant_perms.id).delete()
user_obj.tenants.remove(self)
tenant_user_removed.send(sender=self.__class__, user=user_obj, tenant=self)
def delete_tenant(self):
'''
We don't actually delete the tenant out of the database, but we associate them
with a the public schema user and change their url to reflect their delete
datetime and previous owner
The caller should verify that the user deleting the tenant owns the tenant.
'''
# Prevent public tenant schema from being deleted
if self.schema_name == get_public_schema_name():
raise ValueError("Cannot delete public tenant schema")
for user_obj in self.user_set.all():
# Don't delete owner at this point
if user_obj.id == self.owner.id:
continue
self.remove_user(user_obj)
# Seconds since epoch, time() returns a float, so we convert to
# an int first to truncate the decimal portion
time_string = str(int(time.time()))
new_url = "{}-{}-{}".format(
time_string,
str(self.owner.id),
self.domain_url
)
self.domain_url = new_url
# The schema generated each time (even with same url slug) will be unique.
# So we do not have to worry about a conflict with that
# Set the owner to the system user (public schema owner)
public_tenant = get_tenant_model().objects.get(schema_name=get_public_schema_name())
old_owner = self.owner
# Transfer ownership to system
self.transfer_ownership(public_tenant.owner)
# Remove old owner as a user if the owner still exists after the transfer
if self.user_set.filter(id=user_obj.id).exists():
self.remove_user(old_owner)
@schema_required
def transfer_ownership(self, new_owner):
old_owner = self.owner
# Remove current owner superuser status but retain any assigned role(s)
old_owner_tenant = old_owner.usertenantpermissions
old_owner_tenant.is_superuser = False
old_owner_tenant.save()
self.owner = new_owner
# If original has no permissions left, remove user from tenant
if not old_owner_tenant.groups.exists():
self.remove_user(old_owner)
try:
# Set new user as superuser in this tenant if user already exists
user = self.user_set.get(id=new_owner.id)
user_tenant = user.usertenantpermissions
user_tenant.is_superuser = True
user_tenant.save()
except get_user_model().DoesNotExist:
# New user is not a part of the system, add them as a user..
self.add_user(new_owner, is_superuser=True)
self.save()
class Meta:
abstract = True
class UserProfileManager(BaseUserManager):
def _create_user(self, email, password, is_staff, is_superuser, is_verified, **extra_fields):
# Do some schema validation to protect against calling create user from inside
# a tenant. Must create public tenant permissions during user creation. This
# happens during assign role. This function cannot be used until a public
# schema already exists
UserModel = get_user_model()
if connection.get_schema() != get_public_schema_name():
raise SchemaError("Schema must be public for UserProfileManager user creation")
if not email:
raise ValueError("Users must have an email address.")
# If no password is submitted, just assign a random one to lock down
# the account a little bit.
if not password:
password = self.make_random_password(length=30)
email = self.normalize_email(email)
profile = UserModel.objects.filter(email=email).first()
if profile and profile.is_active:
raise ExistsError("User already exists!")
# Profile might exist but not be active. If a profile does exist
# all previous history logs will still be associated with the user,
# but will not be accessible because the user won't be linked to
# any tenants from the user's previous membership. There are two
# exceptions to this. 1) The user gets re-invited to a tenant it
# previously had access to (this is good thing IMO). 2) The public
# schema if they had previous activity associated would be available
if not profile:
profile = UserModel()
profile.email = email
profile.is_active = True
profile.is_verified = is_verified
profile.set_password(password)
profile.save()
# Get public tenant tenant and link the user (no perms)
public_tenant = get_tenant_model().objects.get(schema_name=get_public_schema_name())
public_tenant.add_user(profile)
# Public tenant permissions object was created when we assigned a
# role to the user above, if we are a staff/superuser we set it here
if is_staff or is_superuser:
user_tenant = profile.usertenantpermissions
user_tenant.is_staff = is_staff
user_tenant.is_superuser = is_superuser
user_tenant.save()
tenant_user_created.send(sender=self.__class__, user=profile)
return profile
def create_user(self, email=None, password=None, is_staff=False, **extra_fields):
return self._create_user(email, password, is_staff, False, False, **extra_fields)
def create_superuser(self, password, email=None, **extra_fields):
return self._create_user(email, password, True, True, True, **extra_fields)
def delete_user(self, user_obj):
if not user_obj.is_active:
raise InactiveError("User specified is not an active user!")
# Check to make sure we don't try to delete the public tenant owner
# that would be bad....
public_tenant = get_tenant_model().objects.get(schema_name=get_public_schema_name())
if user_obj.id == public_tenant.owner.id:
raise DeleteError("Cannot delete the public tenant owner!")
# This includes the linked public tenant 'tenant'. It will delete the
# Tenant permissions and unlink when user is deleted
for tenant in user_obj.tenants.all():
# If user owns the tenant, we call delete on the tenant
# which will delete the user from the tenant as well
if tenant.owner.id == user_obj.id:
# Delete tenant will handle any other linked users to that tenant
tenant.delete_tenant()
else:
# Unlink user from all roles in any tenant it doesn't own
tenant.remove_user(user_obj)
# Set is_active, don't actually delete the object
user_obj.is_active = False
user_obj.save()
tenant_user_deleted.send(sender=self.__class__, user=user_obj)
# This cant be located in the users app otherwise it would get loaded into
# both the public schema and all tenant schemas. We want profiles only
# in the public schema alongside the TenantBase model
class UserProfile(AbstractBaseUser, PermissionsMixinFacade):
"""
An authentication-only model that is in the public tenant schema but
linked from the authorization model (UserTenantPermissions)
where as to allow for one global profile (public schema) for each user
but maintain permissions on a per tenant basis.
To access permissions for a user, the request must come through the
tenant that permissions are desired for.
Requires use of the ModelBackend
"""
USERNAME_FIELD = "email"
objects = UserProfileManager()
tenants = models.ManyToManyField(
settings.TENANT_MODEL,
verbose_name=_('tenants'),
blank=True,
help_text=_('The tenants this user belongs to.'),
related_name="user_set"
)
email = models.EmailField(
_("Email Address"),
unique = True,
db_index = True,
)
is_active = models.BooleanField(_('active'), default=True)
# Tracks whether the user's email has been verified
is_verified = models.BooleanField(_('verified'), default=False)
class Meta:
abstract = True
def has_verified_email(self):
return self.is_verified == True
def delete(self, force_drop=False):
if force_drop:
super(UserProfile, self).delete(force_drop=True)
else:
raise DeleteError("UserProfile.objects.delete_user() should be used.")
def __unicode__(self):
return self.email
def get_short_name(self):
return self.email
def get_full_name(self):
return str(self) # just use __unicode__ here.