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 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
- c = RemoteEvents.hincrby("users:failed:ip", request.ip, 1)
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
- RemoteEvents.hdel("users:failed:ip", request.ip)
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, use_jwt=False):
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
- else:
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.user_ptr.backend = 'django.contrib.auth.backends.ModelBackend'
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
- PersistentLog.log("account blocked, {}".format(reason), 5, request, "account.Member", self.pk, "blocked")
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, use_jwt=True):
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.login(request=request)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-restit
3
- Version: 4.2.175
3
+ Version: 4.2.176
4
4
  Summary: A Rest Framework for DJANGO
5
5
  License: MIT
6
6
  Author: Ian Starnes
@@ -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=qmLCOVbNTRr4L-E7BbOMtv4V64QN7K-0pXDgnuB-AbY,54722
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=0LUTb-dO6wk6rWitPVAXeR0IqpbX5Yw8tHM5U9QJibA,17234
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=5nuojPskvcFtHA31BnuuT6Z7MtZov2YpOn2IHnzm9cw,122
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=Rm40hWDYop0tMqxdN-J2NT-dCnP-f4SfCZxSO02ajzs,7965
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.175.dist-info/LICENSE.md,sha256=VHN4hhEeVOoFjtG-5fVv4jesA4SWi0Z-KgOzzN6a1ps,1068
521
- django_restit-4.2.175.dist-info/METADATA,sha256=DuVZg0LP0bkuNNdfm9IlJXwcpPE-O4-V6qSZYRHICTk,7714
522
- django_restit-4.2.175.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
523
- django_restit-4.2.175.dist-info/RECORD,,
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
@@ -1,4 +1,4 @@
1
1
  from .uberdict import UberDict # noqa: F401
2
2
  from .settings_helper import settings # noqa: F401
3
3
 
4
- __version__ = "4.2.175"
4
+ __version__ = "4.2.176"
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
-