django-restit 4.2.175__py3-none-any.whl → 4.2.176__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- account/models/member.py +42 -29
- account/rpc/auth.py +12 -2
- {django_restit-4.2.175.dist-info → django_restit-4.2.176.dist-info}/METADATA +1 -1
- {django_restit-4.2.175.dist-info → django_restit-4.2.176.dist-info}/RECORD +8 -8
- rest/__init__.py +1 -1
- rest/mail.py +7 -4
- {django_restit-4.2.175.dist-info → django_restit-4.2.176.dist-info}/LICENSE.md +0 -0
- {django_restit-4.2.175.dist-info → django_restit-4.2.176.dist-info}/WHEEL +0 -0
account/models/member.py
CHANGED
@@ -149,7 +149,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
149
149
|
display_name = models.CharField(max_length=64, blank=True, null=True, default=None)
|
150
150
|
picture = models.ForeignKey("medialib.MediaItem", blank=True, null=True, help_text="Profile picture", related_name='+', on_delete=models.CASCADE)
|
151
151
|
|
152
|
-
# we use this token to allow us to invalidate JWT tokens
|
152
|
+
# we use this token to allow us to invalidate JWT tokens
|
153
153
|
security_token = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
|
154
154
|
auth_code = models.CharField(max_length=64, blank=True, null=True, default=None, db_index=True)
|
155
155
|
auth_code_expires = models.DateTimeField(blank=True, null=True, default=None)
|
@@ -263,6 +263,24 @@ class Member(User, RestModel, MetaDataModel):
|
|
263
263
|
return self.has_usable_password()
|
264
264
|
return False
|
265
265
|
|
266
|
+
def getFailedLoginCount(self):
|
267
|
+
c = RemoteEvents.hget("users:failed:username", self.username)
|
268
|
+
if c is not None:
|
269
|
+
return int(c)
|
270
|
+
return c
|
271
|
+
|
272
|
+
@classmethod
|
273
|
+
def GetFailedLoginsForIP(cls, ip):
|
274
|
+
c = RemoteEvents.hget("users:failed:ip", ip)
|
275
|
+
if c is not None:
|
276
|
+
return int(c)
|
277
|
+
return c
|
278
|
+
|
279
|
+
@classmethod
|
280
|
+
def GetIPsWithFailedLoginAttempts(cls, threshold):
|
281
|
+
failed_ips = RemoteEvents.hgetall("users:failed:ip")
|
282
|
+
return {ip: int(count) for ip, count in failed_ips.items() if int(count) > threshold}
|
283
|
+
|
266
284
|
def recordFailedLogin(self, request):
|
267
285
|
c = RemoteEvents.hincrby("users:failed:username", self.username, 1)
|
268
286
|
if c >= settings.LOCK_PASSWORD_ATTEMPTS:
|
@@ -273,13 +291,15 @@ class Member(User, RestModel, MetaDataModel):
|
|
273
291
|
"account", f"incorrect password for {self.username}", level=8,
|
274
292
|
error_code=498,
|
275
293
|
request=request)
|
276
|
-
|
294
|
+
ic = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
|
295
|
+
return c
|
277
296
|
|
278
297
|
def recordSuccessLogin(self, request):
|
279
298
|
self.last_login = datetime.now()
|
280
299
|
self.save()
|
281
300
|
RemoteEvents.hdel("users:failed:username", self.username)
|
282
|
-
|
301
|
+
if request:
|
302
|
+
RemoteEvents.hdel("users:failed:ip", request.ip)
|
283
303
|
|
284
304
|
def hasPasswordExpired(self):
|
285
305
|
now = datetime.now()
|
@@ -288,38 +308,33 @@ class Member(User, RestModel, MetaDataModel):
|
|
288
308
|
self.save()
|
289
309
|
return now - self.password_changed > timedelta(days=settings.PASSWORD_EXPIRES_DAYS)
|
290
310
|
|
291
|
-
def login(self, password=None, request=None
|
292
|
-
if not self.is_active or self.is_blocked:
|
293
|
-
return False
|
294
|
-
# can force login
|
311
|
+
def login(self, password=None, request=None):
|
295
312
|
if not request:
|
296
313
|
request = rh.getActiveRequest()
|
314
|
+
if not self.is_active or self.is_blocked:
|
315
|
+
return False
|
297
316
|
if not self.checkPassword(password):
|
298
|
-
# invalid password
|
299
317
|
self.recordFailedLogin(request)
|
300
318
|
return False
|
301
|
-
|
302
|
-
self.recordSuccessLogin(request)
|
303
|
-
if use_jwt:
|
304
|
-
self.recordSuccessLogin(request)
|
305
|
-
self.locateByIP(request.ip)
|
306
|
-
return True
|
307
|
-
self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
|
308
|
-
auth_login(request, self.user_ptr)
|
309
|
-
self.locateByIP(request.ip)
|
319
|
+
self.authLogin(request)
|
310
320
|
return True
|
311
321
|
|
312
322
|
def loginNoPassword(self, request=None):
|
313
323
|
if not self.is_active or self.is_blocked:
|
314
324
|
return False
|
315
|
-
# can force login
|
316
325
|
if not request:
|
317
326
|
request = rh.getActiveRequest()
|
318
|
-
self.
|
319
|
-
auth_login(request, self.user_ptr)
|
320
|
-
self.locateByIP(request.ip)
|
327
|
+
self.authLogin(request)
|
321
328
|
return True
|
322
329
|
|
330
|
+
def authLogin(self, request, use_session=True):
|
331
|
+
if use_session:
|
332
|
+
self.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
|
333
|
+
auth_login(request, self.user_ptr)
|
334
|
+
if request:
|
335
|
+
self.locateByIP(request.ip)
|
336
|
+
self.recordSuccessLogin(request)
|
337
|
+
|
323
338
|
def canLogin(self, request=None, throw_exception=True, using_password=True):
|
324
339
|
if not self.is_active:
|
325
340
|
self.log("login_blocked", F"account is not active {self.username}", request, method="login", level=31)
|
@@ -577,7 +592,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
577
592
|
SMS.send(phone, msg)
|
578
593
|
return True
|
579
594
|
|
580
|
-
def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
|
595
|
+
def sendEmail(self, subject, body, attachments=[], do_async=True, template=None,
|
581
596
|
context=None):
|
582
597
|
default_domain = self.getProperty("default_domain", settings.EMAIL_DEFAULT_DOMAIN)
|
583
598
|
from_email = rh.getFromEmailForHost(default_domain)
|
@@ -675,7 +690,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
675
690
|
allow_email = not self.email_disabled and valid_email and (force or via in ["all", "email"])
|
676
691
|
else:
|
677
692
|
allow_email = valid_email
|
678
|
-
|
693
|
+
|
679
694
|
if not allow_email and not allow_sms:
|
680
695
|
return False
|
681
696
|
|
@@ -796,7 +811,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
796
811
|
def block(self, reason, request=None):
|
797
812
|
if not request:
|
798
813
|
request = self.getActiveRequest()
|
799
|
-
|
814
|
+
self.auditLog(f"account blocked, {reason}", "blocked", level=5)
|
800
815
|
RemoteEvents.hset("users:blocked:username", self.username, time.time())
|
801
816
|
self.reportIncident(
|
802
817
|
"account", f"account '{self.username}' blocked: {reason}", level=2,
|
@@ -832,7 +847,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
832
847
|
|
833
848
|
# BEGIN REST CALLBACKS
|
834
849
|
def on_permission_change(self, key, value, old_value, category):
|
835
|
-
# called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
|
850
|
+
# called when a metadata.permissions field changes.. see django settings USER_METADATA_PROPERTIES
|
836
851
|
# we want to log both the person changing permissions
|
837
852
|
# and those being changed
|
838
853
|
request = RestModel.getActiveRequest()
|
@@ -865,7 +880,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
865
880
|
request = self.getActiveRequest()
|
866
881
|
if not request.member.is_superuser:
|
867
882
|
raise PermissionDeniedException("Permission Denied: attempting to super user")
|
868
|
-
self.is_superuser = int(value)
|
883
|
+
self.is_superuser = int(value)
|
869
884
|
|
870
885
|
def set_disable(self, value):
|
871
886
|
if value is not None and value in [1, '1', True, 'true']:
|
@@ -1219,7 +1234,7 @@ class Member(User, RestModel, MetaDataModel):
|
|
1219
1234
|
def authWS4RedisConnection(cls, auth_data):
|
1220
1235
|
"""
|
1221
1236
|
This method is used by the async/websocket service to authenticate.
|
1222
|
-
If the model can authenticate the connection it should return dict
|
1237
|
+
If the model can authenticate the connection it should return dict
|
1223
1238
|
with kind and pk of the model that is authenticaed
|
1224
1239
|
"""
|
1225
1240
|
from rest import UberDict
|
@@ -1338,5 +1353,3 @@ class AuthToken(models.Model, RestModel):
|
|
1338
1353
|
|
1339
1354
|
def __str__(self):
|
1340
1355
|
return "{o.member}: {o.secure_token}".format(o=self)
|
1341
|
-
|
1342
|
-
|
account/rpc/auth.py
CHANGED
@@ -55,7 +55,7 @@ def jwt_login(request):
|
|
55
55
|
resp = checkForTOTP(request, member)
|
56
56
|
if resp is not None:
|
57
57
|
return resp
|
58
|
-
if not member.login(request=request, password=password
|
58
|
+
if not member.login(request=request, password=password):
|
59
59
|
# we do not want permission denied catcher invoked as it is already handled in login method
|
60
60
|
return rv.restStatus(request, False, error=f"Invalid Credentials {username}", error_code=401)
|
61
61
|
return on_complete_jwt(request, member, auth_method)
|
@@ -221,7 +221,8 @@ def member_login_uname_code(request, username, auth_code):
|
|
221
221
|
member.setPassword(password)
|
222
222
|
member.auth_code = None
|
223
223
|
member.auth_code_expires = None
|
224
|
-
member.
|
224
|
+
member.canLogin(request, using_password=False) # throws exception if cannot login
|
225
|
+
member.loginNoPassword(request)
|
225
226
|
member.save()
|
226
227
|
member.log("code_login", "code login", request, method="login", level=8)
|
227
228
|
if request.DATA.get("auth_method") == "basic" and ALLOW_BASIC_LOGIN:
|
@@ -411,3 +412,12 @@ def totp_verify(request):
|
|
411
412
|
return rv.restPermissionDenied(request, "invalid code")
|
412
413
|
request.member.setProperty("totp_verified", 1)
|
413
414
|
return rv.restStatus(request, True)
|
415
|
+
|
416
|
+
|
417
|
+
# time based one time passwords
|
418
|
+
@rd.urlPOST('security/ip/failed_logins')
|
419
|
+
@rd.login_required
|
420
|
+
def failed_logins_by_ip(request):
|
421
|
+
ips = am.Member.GetIPsWithFailedLoginAttempts(request.DATA.get("threshold", 10))
|
422
|
+
lst = [dict(ip=k, count=v) for k, v in ips.items()]
|
423
|
+
return rv.restList(request, lst)
|
@@ -31,7 +31,7 @@ account/models/device.py,sha256=8D-Sbv9PZWAnX6UVpp1lNJ03P24fknNnN1VOhqY7RVg,6306
|
|
31
31
|
account/models/feeds.py,sha256=vI7fG4ASY1M0Zjke24RdnfDcuWeATl_yR_25jPmT64g,2011
|
32
32
|
account/models/group.py,sha256=JVyMIakLskUuGXBJFyessw4LlD9Fl6AsHtpo1yZEYjk,22884
|
33
33
|
account/models/legacy.py,sha256=zYdtv4LC0ooxPVqWM-uToPwV-lYWQLorSE6p6yn1xDw,2720
|
34
|
-
account/models/member.py,sha256=
|
34
|
+
account/models/member.py,sha256=6-KbOYFyvloyiITgz5gKMyORmPtDESdzbBbMrjmz4Rs,55058
|
35
35
|
account/models/membership.py,sha256=90EpAhOsGaqphDAkONP6j_qQ0OWSRaQsI8H7E7fgMkE,9249
|
36
36
|
account/models/notify.py,sha256=YKYEXT56i98b7-ydLt5UuEVOqW7lipQMi-KuiPhcSwY,15627
|
37
37
|
account/models/passkeys.py,sha256=lObapudvL--ABSTZTIELmYvHE3dPF0tO_KmuYk0ZJXc,1699
|
@@ -43,7 +43,7 @@ account/passkeys/__init__.py,sha256=FwXYJXwSJXfkLojGBcVpF1dFpgFhzDdd9N_3naYQ0cc,
|
|
43
43
|
account/passkeys/core.py,sha256=4aUBNCuF_kjOvE1zFapK1Pj28ap5slO71dRyfnWi0YU,4148
|
44
44
|
account/periodic.py,sha256=-u0n-7QTJgDOkasGhBAPwHAwjpqWGA-MZLEFkVTqCGU,874
|
45
45
|
account/rpc/__init__.py,sha256=SGF0M_-H0dKh3b1apSX29BotNWAvITYccGQVC0MIjL8,336
|
46
|
-
account/rpc/auth.py,sha256=
|
46
|
+
account/rpc/auth.py,sha256=MDXXhTlzTRcKB8TQ0OPawvsb0P4EeH_J3ukeSljgytk,17615
|
47
47
|
account/rpc/device.py,sha256=lU2BHNPreHV0dDTjAPc7Sc-5m2JP8SiWVqiKuBfV7Fo,2281
|
48
48
|
account/rpc/group.py,sha256=hw7iczZ6W_IrRbx5ZDw6cZ5I_ztqxhtUFJD9WR91_4s,4948
|
49
49
|
account/rpc/member.py,sha256=8XnJX-iri0Om4nc-V2_tDJzfCSzziKLw6dUx9egtEZE,2236
|
@@ -380,7 +380,7 @@ pushit/utils.py,sha256=IeTCGa-164nmB1jIsK1lu1O1QzUhS3BKfuXHGjCW-ck,2121
|
|
380
380
|
rest/.gitignore,sha256=TbEvWRMnAiajCTOdhiNrd9eeCAaIjRp9PRjE_VkMM5g,118
|
381
381
|
rest/README.md,sha256=V3ETc-cJu8PZIbKr9xSe_pA4JEUpC8Dhw4bQeVCDJPw,5460
|
382
382
|
rest/RemoteEvents.py,sha256=nL46U7AuxIrlw2JunphR1tsXyqi-ep_gD9CYGpYbNgE,72
|
383
|
-
rest/__init__.py,sha256=
|
383
|
+
rest/__init__.py,sha256=57yJpEKX4zpzd53NtME1OKpJzJqzJxz38KVjs86WJ0g,122
|
384
384
|
rest/arc4.py,sha256=y644IbF1ec--e4cUJ3KEYsewTCITK0gmlwa5mJruFC0,1967
|
385
385
|
rest/cache.py,sha256=1Qg0rkaCJCaVP0-l5hZg2CIblTdeBSlj_0fP6vlKUpU,83
|
386
386
|
rest/crypto/__init__.py,sha256=Tl0U11rgj1eBYqd6OXJ2_XSdNLumW_JkBZnaJqI6Ldw,72
|
@@ -400,7 +400,7 @@ rest/helpers.py,sha256=t7smlOUzchVno-zeq7xMJIwogAR2DeSrffWxgysOHX8,29531
|
|
400
400
|
rest/joke.py,sha256=0PpKaX2iN7jlS62kgjfmmqkFBYLPURz15aQ8R7OJkJ8,260
|
401
401
|
rest/jwtoken.py,sha256=F7Vvpm31rAplTXr8XFP-Lb4BnDB3j1B2nQq0P1iTCLQ,2576
|
402
402
|
rest/log.py,sha256=hd1_4HBOS395sfXJIL6BTw9yekm1SLgBwYx_PdfIhKA,20930
|
403
|
-
rest/mail.py,sha256=
|
403
|
+
rest/mail.py,sha256=FFvHgNcq84sJP5DyaJQKbuoao5NAsS6r-PT4MRfjWFU,8139
|
404
404
|
rest/mailman.py,sha256=v5O1G5s3HiAKmz-J1z0uT6_q3xsONPpxVl9saEyQQ2I,9174
|
405
405
|
rest/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
406
406
|
rest/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -517,7 +517,7 @@ ws4redis/servers/uwsgi.py,sha256=VyhoCI1DnVFqBiJYHoxqn5Idlf6uJPHvfBKgkjs34mo,172
|
|
517
517
|
ws4redis/settings.py,sha256=KKq00EwoGnz1yLwCZr5Dfoq2izivmAdsNEEM4EhZwN4,1610
|
518
518
|
ws4redis/utf8validator.py,sha256=S0OlfjeGRP75aO6CzZsF4oTjRQAgR17OWE9rgZdMBZA,5122
|
519
519
|
ws4redis/websocket.py,sha256=R0TUyPsoVRD7Y_oU7w2I6NL4fPwiz5Vl94-fUkZgLHA,14848
|
520
|
-
django_restit-4.2.
|
521
|
-
django_restit-4.2.
|
522
|
-
django_restit-4.2.
|
523
|
-
django_restit-4.2.
|
520
|
+
django_restit-4.2.176.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
|
521
|
+
django_restit-4.2.176.dist-info/METADATA,sha256=WVtqfF02L5jyZtQQoObLI57ZpcubGld7ZDyqc1vSXlg,7714
|
522
|
+
django_restit-4.2.176.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
523
|
+
django_restit-4.2.176.dist-info/RECORD,,
|
rest/__init__.py
CHANGED
rest/mail.py
CHANGED
@@ -37,11 +37,16 @@ SES_REGION = settings.SES_REGION
|
|
37
37
|
EMAIL_METRICS = settings.get("EMAIL_METRICS", False)
|
38
38
|
|
39
39
|
|
40
|
-
def send(to, subject, body=None, attachments=[],
|
40
|
+
def send(to, subject, body=None, attachments=[],
|
41
41
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
42
|
-
fail_silently=True, template=None,
|
42
|
+
fail_silently=True, template=None,
|
43
43
|
context=None, do_async=False, replyto=None):
|
44
44
|
# make sure to is list
|
45
|
+
if isinstance(to, str):
|
46
|
+
if "," in to:
|
47
|
+
to = [t.strip() for t in to.split(',')]
|
48
|
+
elif ";" in to:
|
49
|
+
to = [t.strip() for t in to.split(';')]
|
45
50
|
if not isinstance(to, (tuple, list)):
|
46
51
|
to = [to]
|
47
52
|
# if template lets render
|
@@ -254,5 +259,3 @@ def render_to_mail(name, context):
|
|
254
259
|
body = html_content
|
255
260
|
|
256
261
|
send(toaddrs, subject, body=body, from_email=fromaddr, do_async=True, replyto=replyto)
|
257
|
-
|
258
|
-
|
File without changes
|
File without changes
|