django-nativemojo 0.1.10__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.
Files changed (194) hide show
  1. django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
  2. django_nativemojo-0.1.10.dist-info/METADATA +96 -0
  3. django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
  4. django_nativemojo-0.1.10.dist-info/RECORD +194 -0
  5. django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
  6. mojo/__init__.py +3 -0
  7. mojo/apps/account/__init__.py +1 -0
  8. mojo/apps/account/admin.py +91 -0
  9. mojo/apps/account/apps.py +16 -0
  10. mojo/apps/account/migrations/0001_initial.py +77 -0
  11. mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
  12. mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
  13. mojo/apps/account/migrations/__init__.py +0 -0
  14. mojo/apps/account/models/__init__.py +3 -0
  15. mojo/apps/account/models/group.py +98 -0
  16. mojo/apps/account/models/member.py +95 -0
  17. mojo/apps/account/models/pkey.py +18 -0
  18. mojo/apps/account/models/user.py +211 -0
  19. mojo/apps/account/rest/__init__.py +3 -0
  20. mojo/apps/account/rest/group.py +25 -0
  21. mojo/apps/account/rest/user.py +47 -0
  22. mojo/apps/account/utils/__init__.py +0 -0
  23. mojo/apps/account/utils/jwtoken.py +72 -0
  24. mojo/apps/account/utils/passkeys.py +54 -0
  25. mojo/apps/fileman/README.md +549 -0
  26. mojo/apps/fileman/__init__.py +0 -0
  27. mojo/apps/fileman/apps.py +15 -0
  28. mojo/apps/fileman/backends/__init__.py +117 -0
  29. mojo/apps/fileman/backends/base.py +319 -0
  30. mojo/apps/fileman/backends/filesystem.py +397 -0
  31. mojo/apps/fileman/backends/s3.py +398 -0
  32. mojo/apps/fileman/examples/configurations.py +378 -0
  33. mojo/apps/fileman/examples/usage_example.py +665 -0
  34. mojo/apps/fileman/management/__init__.py +1 -0
  35. mojo/apps/fileman/management/commands/__init__.py +1 -0
  36. mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
  37. mojo/apps/fileman/models/__init__.py +7 -0
  38. mojo/apps/fileman/models/file.py +292 -0
  39. mojo/apps/fileman/models/manager.py +227 -0
  40. mojo/apps/fileman/models/render.py +0 -0
  41. mojo/apps/fileman/rest/__init__ +0 -0
  42. mojo/apps/fileman/rest/__init__.py +23 -0
  43. mojo/apps/fileman/rest/fileman.py +13 -0
  44. mojo/apps/fileman/rest/upload.py +92 -0
  45. mojo/apps/fileman/utils/__init__.py +19 -0
  46. mojo/apps/fileman/utils/upload.py +616 -0
  47. mojo/apps/incident/__init__.py +1 -0
  48. mojo/apps/incident/handlers/__init__.py +3 -0
  49. mojo/apps/incident/handlers/event_handlers.py +142 -0
  50. mojo/apps/incident/migrations/0001_initial.py +83 -0
  51. mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
  52. mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
  53. mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
  54. mojo/apps/incident/migrations/__init__.py +0 -0
  55. mojo/apps/incident/models/__init__.py +3 -0
  56. mojo/apps/incident/models/event.py +135 -0
  57. mojo/apps/incident/models/incident.py +33 -0
  58. mojo/apps/incident/models/rule.py +247 -0
  59. mojo/apps/incident/parsers/__init__.py +0 -0
  60. mojo/apps/incident/parsers/ossec/__init__.py +1 -0
  61. mojo/apps/incident/parsers/ossec/core.py +82 -0
  62. mojo/apps/incident/parsers/ossec/parsed.py +23 -0
  63. mojo/apps/incident/parsers/ossec/rules.py +124 -0
  64. mojo/apps/incident/parsers/ossec/utils.py +169 -0
  65. mojo/apps/incident/reporter.py +42 -0
  66. mojo/apps/incident/rest/__init__.py +2 -0
  67. mojo/apps/incident/rest/event.py +23 -0
  68. mojo/apps/incident/rest/ossec.py +22 -0
  69. mojo/apps/logit/__init__.py +0 -0
  70. mojo/apps/logit/admin.py +37 -0
  71. mojo/apps/logit/migrations/0001_initial.py +32 -0
  72. mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
  73. mojo/apps/logit/migrations/0003_log_level.py +18 -0
  74. mojo/apps/logit/migrations/__init__.py +0 -0
  75. mojo/apps/logit/models/__init__.py +1 -0
  76. mojo/apps/logit/models/log.py +57 -0
  77. mojo/apps/logit/rest.py +9 -0
  78. mojo/apps/metrics/README.md +79 -0
  79. mojo/apps/metrics/__init__.py +12 -0
  80. mojo/apps/metrics/redis_metrics.py +331 -0
  81. mojo/apps/metrics/rest/__init__.py +1 -0
  82. mojo/apps/metrics/rest/base.py +152 -0
  83. mojo/apps/metrics/rest/db.py +0 -0
  84. mojo/apps/metrics/utils.py +227 -0
  85. mojo/apps/notify/README.md +91 -0
  86. mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
  87. mojo/apps/notify/__init__.py +0 -0
  88. mojo/apps/notify/admin.py +52 -0
  89. mojo/apps/notify/handlers/__init__.py +0 -0
  90. mojo/apps/notify/handlers/example_handlers.py +516 -0
  91. mojo/apps/notify/handlers/ses/__init__.py +25 -0
  92. mojo/apps/notify/handlers/ses/bounce.py +0 -0
  93. mojo/apps/notify/handlers/ses/complaint.py +25 -0
  94. mojo/apps/notify/handlers/ses/message.py +86 -0
  95. mojo/apps/notify/management/__init__.py +0 -0
  96. mojo/apps/notify/management/commands/__init__.py +1 -0
  97. mojo/apps/notify/management/commands/process_notifications.py +370 -0
  98. mojo/apps/notify/mod +0 -0
  99. mojo/apps/notify/models/__init__.py +12 -0
  100. mojo/apps/notify/models/account.py +128 -0
  101. mojo/apps/notify/models/attachment.py +24 -0
  102. mojo/apps/notify/models/bounce.py +68 -0
  103. mojo/apps/notify/models/complaint.py +40 -0
  104. mojo/apps/notify/models/inbox.py +113 -0
  105. mojo/apps/notify/models/inbox_message.py +173 -0
  106. mojo/apps/notify/models/outbox.py +129 -0
  107. mojo/apps/notify/models/outbox_message.py +288 -0
  108. mojo/apps/notify/models/template.py +30 -0
  109. mojo/apps/notify/providers/__init__.py +0 -0
  110. mojo/apps/notify/providers/aws.py +73 -0
  111. mojo/apps/notify/rest/__init__.py +0 -0
  112. mojo/apps/notify/rest/ses.py +0 -0
  113. mojo/apps/notify/utils/__init__.py +2 -0
  114. mojo/apps/notify/utils/notifications.py +404 -0
  115. mojo/apps/notify/utils/parsing.py +202 -0
  116. mojo/apps/notify/utils/render.py +144 -0
  117. mojo/apps/tasks/README.md +118 -0
  118. mojo/apps/tasks/__init__.py +11 -0
  119. mojo/apps/tasks/manager.py +489 -0
  120. mojo/apps/tasks/rest/__init__.py +2 -0
  121. mojo/apps/tasks/rest/hooks.py +0 -0
  122. mojo/apps/tasks/rest/tasks.py +62 -0
  123. mojo/apps/tasks/runner.py +174 -0
  124. mojo/apps/tasks/tq_handlers.py +14 -0
  125. mojo/decorators/__init__.py +3 -0
  126. mojo/decorators/auth.py +25 -0
  127. mojo/decorators/cron.py +31 -0
  128. mojo/decorators/http.py +132 -0
  129. mojo/decorators/validate.py +14 -0
  130. mojo/errors.py +88 -0
  131. mojo/helpers/__init__.py +0 -0
  132. mojo/helpers/aws/__init__.py +0 -0
  133. mojo/helpers/aws/client.py +8 -0
  134. mojo/helpers/aws/s3.py +268 -0
  135. mojo/helpers/aws/setup_email.py +0 -0
  136. mojo/helpers/cron.py +79 -0
  137. mojo/helpers/crypto/__init__.py +4 -0
  138. mojo/helpers/crypto/aes.py +60 -0
  139. mojo/helpers/crypto/hash.py +59 -0
  140. mojo/helpers/crypto/privpub/__init__.py +1 -0
  141. mojo/helpers/crypto/privpub/hybrid.py +97 -0
  142. mojo/helpers/crypto/privpub/rsa.py +104 -0
  143. mojo/helpers/crypto/sign.py +36 -0
  144. mojo/helpers/crypto/too.l.py +25 -0
  145. mojo/helpers/crypto/utils.py +26 -0
  146. mojo/helpers/daemon.py +94 -0
  147. mojo/helpers/dates.py +69 -0
  148. mojo/helpers/dns/__init__.py +0 -0
  149. mojo/helpers/dns/godaddy.py +62 -0
  150. mojo/helpers/filetypes.py +128 -0
  151. mojo/helpers/logit.py +310 -0
  152. mojo/helpers/modules.py +95 -0
  153. mojo/helpers/paths.py +63 -0
  154. mojo/helpers/redis.py +10 -0
  155. mojo/helpers/request.py +89 -0
  156. mojo/helpers/request_parser.py +269 -0
  157. mojo/helpers/response.py +14 -0
  158. mojo/helpers/settings.py +146 -0
  159. mojo/helpers/sysinfo.py +140 -0
  160. mojo/helpers/ua.py +0 -0
  161. mojo/middleware/__init__.py +0 -0
  162. mojo/middleware/auth.py +26 -0
  163. mojo/middleware/logging.py +55 -0
  164. mojo/middleware/mojo.py +21 -0
  165. mojo/migrations/0001_initial.py +32 -0
  166. mojo/migrations/__init__.py +0 -0
  167. mojo/models/__init__.py +2 -0
  168. mojo/models/meta.py +262 -0
  169. mojo/models/rest.py +538 -0
  170. mojo/models/secrets.py +59 -0
  171. mojo/rest/__init__.py +1 -0
  172. mojo/rest/info.py +26 -0
  173. mojo/serializers/__init__.py +0 -0
  174. mojo/serializers/models.py +165 -0
  175. mojo/serializers/openapi.py +188 -0
  176. mojo/urls.py +38 -0
  177. mojo/ws4redis/README.md +174 -0
  178. mojo/ws4redis/__init__.py +2 -0
  179. mojo/ws4redis/client.py +283 -0
  180. mojo/ws4redis/connection.py +327 -0
  181. mojo/ws4redis/exceptions.py +32 -0
  182. mojo/ws4redis/redis.py +183 -0
  183. mojo/ws4redis/servers/__init__.py +0 -0
  184. mojo/ws4redis/servers/base.py +86 -0
  185. mojo/ws4redis/servers/django.py +171 -0
  186. mojo/ws4redis/servers/uwsgi.py +63 -0
  187. mojo/ws4redis/settings.py +45 -0
  188. mojo/ws4redis/utf8validator.py +128 -0
  189. mojo/ws4redis/websocket.py +403 -0
  190. testit/__init__.py +0 -0
  191. testit/client.py +147 -0
  192. testit/faker.py +20 -0
  193. testit/helpers.py +198 -0
  194. testit/runner.py +262 -0
@@ -0,0 +1,98 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel, MojoSecrets
3
+ from mojo.helpers import dates
4
+
5
+
6
+
7
+ class Group(MojoSecrets, MojoModel):
8
+ """
9
+ Group model.
10
+ """
11
+ created = models.DateTimeField(auto_now_add=True, editable=False)
12
+ modified = models.DateTimeField(auto_now=True, db_index=True)
13
+
14
+ name = models.CharField(max_length=200)
15
+ uuid = models.CharField(max_length=200, null=True, default=None, db_index=True)
16
+ is_active = models.BooleanField(default=True, db_index=True)
17
+ kind = models.CharField(max_length=80, default="group", db_index=True)
18
+
19
+ parent = models.ForeignKey("account.Group", null=True, related_name="groups",
20
+ default=None, on_delete=models.CASCADE)
21
+
22
+ # JSON-based metadata field
23
+ metadata = models.JSONField(default=dict, blank=True)
24
+
25
+ class RestMeta:
26
+ SEARCH_FIELDS = ["name"]
27
+ VIEW_PERMS = ["view_groups", "manage_groups"]
28
+ SAVE_PERMS = ["manage_groups"]
29
+ LIST_DEFAULT_FILTERS = {
30
+ "is_active": True
31
+ }
32
+ GRAPHS = {
33
+ "basic": {
34
+ "fields": [
35
+ 'id',
36
+ 'name',
37
+ 'created',
38
+ 'modified',
39
+ 'is_active',
40
+ 'kind',
41
+ ]
42
+ },
43
+ "default": {
44
+ "fields": [
45
+ 'id',
46
+ 'name',
47
+ 'created',
48
+ 'modified',
49
+ 'is_active',
50
+ 'kind',
51
+ 'parent',
52
+ 'metadata'
53
+ ]
54
+ },
55
+ "graphs": {
56
+ "parent": "basic"
57
+ }
58
+ }
59
+
60
+ @property
61
+ def timezone(self):
62
+ return self.metadata.get("timezone", "America/Los_Angeles")
63
+
64
+ def get_local_day(self, dt_utc=None):
65
+ return dates.get_local_day(self.timezone, dt_utc)
66
+
67
+ def get_local_time(self, dt_utc):
68
+ return dates.get_local_time(self.timezone, dt_utc)
69
+
70
+ def __str__(self):
71
+ return self.name
72
+
73
+ def has_permission(self, user):
74
+ from mojo.account.models.member import GroupMember
75
+ return GroupMember.objects.filter(user=user).last()
76
+
77
+ def member_has_permission(self, user, perms, check_user=True):
78
+ if check_user and user.has_permission(perms):
79
+ return True
80
+ ms = self.has_permission(user)
81
+ if ms is not None:
82
+ return ms.has_permission(perms)
83
+ return False
84
+
85
+ def get_metadata(self):
86
+ # converts our local metadata into an objict
87
+ self.metadata = self.jsonfield_as_objict("metadata")
88
+ return self.metadata
89
+
90
+ def get_member_for_user(self, user):
91
+ return self.members.filter(user=user).last()
92
+
93
+ @classmethod
94
+ def on_rest_handle_list(cls, request):
95
+ if cls.rest_check_permission(request, "VIEW_PERMS"):
96
+ return cls.on_rest_list(request)
97
+ group_ids = request.user.members.values_list('group__id', flat=True)
98
+ return cls.on_rest_list(request, cls.objects.filter(id__in=group_ids))
@@ -0,0 +1,95 @@
1
+ from django.db import models
2
+ from mojo.models import MojoModel
3
+ from mojo import errors as merrors
4
+ from mojo.helpers.settings import settings
5
+
6
+ MEMBER_PERMS_PROTECTION = settings.get("MEMBER_PERMS_PROTECTION", {})
7
+
8
+
9
+ class GroupMember(models.Model, MojoModel):
10
+ """
11
+ A member of a group
12
+ """
13
+ created = models.DateTimeField(auto_now_add=True, editable=False)
14
+ modified = models.DateTimeField(auto_now=True, db_index=True)
15
+ user = models.ForeignKey(
16
+ "account.User",related_name="members",
17
+ on_delete=models.CASCADE)
18
+ group = models.ForeignKey(
19
+ "account.Group", related_name="members",
20
+ on_delete=models.CASCADE)
21
+ is_active = models.BooleanField(default=True, db_index=True)
22
+ # JSON-based permissions field
23
+ permissions = models.JSONField(default=dict, blank=True)
24
+ # JSON-based metadata field
25
+ metadata = models.JSONField(default=dict, blank=True)
26
+
27
+ class RestMeta:
28
+ VIEW_PERMS = ["view_groups", "manage_groups"]
29
+ SAVE_PERMS = ["manage_groups"]
30
+ LIST_DEFAULT_FILTERS = {
31
+ "is_active": True
32
+ }
33
+ GRAPHS = {
34
+ "default": {
35
+ "fields": [
36
+ 'id',
37
+ 'name',
38
+ 'created',
39
+ 'modified',
40
+ 'is_active',
41
+ 'permissions',
42
+ 'metadata'
43
+ ],
44
+ "graphs": {
45
+ "user": "basic",
46
+ "group": "basic"
47
+ }
48
+ }
49
+ }
50
+
51
+ def __str__(self):
52
+ return f"{self.user.username}@{self.group.name}"
53
+
54
+ def can_change_permission(self, perm, value, request):
55
+ if request.user.has_permission(["manage_groups", "manage_users"]):
56
+ return True
57
+ req_member = self.group.get_member_for_user(request.user)
58
+ if req_member is not None:
59
+ if perm in MEMBER_PERMS_PROTECTION:
60
+ return req_member.has_permission(MEMBER_PERMS_PROTECTION[perm])
61
+ return req_member.has_permission(["manage_group", "manage_members"])
62
+ return False
63
+
64
+ def set_permissions(self, value, request):
65
+ if not isinstance(value, dict):
66
+ return
67
+ for perm, perm_value in value.items():
68
+ if not self.can_change_permission(perm, perm_value, request):
69
+ raise merrors.PermissionDeniedException()
70
+ if bool(perm_value):
71
+ self.add_permission(perm)
72
+ else:
73
+ self.remove_permission(perm)
74
+
75
+ def has_permission(self, perm_key):
76
+ """Check if user has a specific permission in JSON field."""
77
+ if isinstance(perm_key, list):
78
+ for pk in perm_key:
79
+ if self.has_permission(pk):
80
+ return True
81
+ return False
82
+ if perm_key == "all":
83
+ return True
84
+ return self.permissions.get(perm_key, False)
85
+
86
+ def add_permission(self, perm_key, value=True):
87
+ """Dynamically add a permission."""
88
+ self.permissions[perm_key] = value
89
+ self.save()
90
+
91
+ def remove_permission(self, perm_key):
92
+ """Remove a permission."""
93
+ if perm_key in self.permissions:
94
+ del self.permissions[perm_key]
95
+ self.save()
@@ -0,0 +1,18 @@
1
+ from django.db import models
2
+
3
+ class Passkey(models.Model):
4
+ user = models.ForeignKey(
5
+ "account.User",related_name="members",
6
+ on_delete=models.CASCADE)
7
+ token = models.TextField()
8
+
9
+ credential_id = models.CharField(max_length=255, unique=True)
10
+ rp_id = models.CharField(max_length=255, null=False, db_index=True)
11
+ is_enabled = models.BooleanField(default=True, db_index=True)
12
+
13
+ created = models.DateTimeField(auto_now_add=True)
14
+ modified = models.DateTimeField(auto_now=True)
15
+ last_used = models.DateTimeField(null=True, blank=True, default=None)
16
+
17
+ def __str__(self):
18
+ return f"{self.user.username} - {self.credential_id} - {self.rp_id}"
@@ -0,0 +1,211 @@
1
+ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
2
+ from django.db import models
3
+ from mojo.models import MojoModel, MojoSecrets
4
+ from mojo.helpers.settings import settings
5
+ from mojo import errors as merrors
6
+ from mojo.helpers import dates
7
+ from mojo.apps.account.utils.jwtoken import JWToken
8
+ import uuid
9
+
10
+ USER_PERMS_PROTECTION = settings.get("USER_PERMS_PROTECTION", {})
11
+ USER_LAST_ACTIVITY_FREQ = settings.get("USER_LAST_ACTIVITY_FREQ", 300)
12
+
13
+ class CustomUserManager(BaseUserManager):
14
+ def create_user(self, email, password=None, **extra_fields):
15
+ if not email:
16
+ raise ValueError("The Email field must be set")
17
+ email = self.normalize_email(email)
18
+ user = self.model(email=email, **extra_fields)
19
+ user.set_password(password)
20
+ user.save(using=self._db)
21
+ return user
22
+
23
+ def create_superuser(self, email, password=None, **extra_fields):
24
+ extra_fields.setdefault("is_staff", True)
25
+ extra_fields.setdefault("is_superuser", True)
26
+ return self.create_user(email, password, **extra_fields)
27
+
28
+ def get_by_natural_key(self, username):
29
+ """Required for Django authentication"""
30
+ return self.get(**{self.model.USERNAME_FIELD: username})
31
+
32
+ class User(MojoSecrets, AbstractBaseUser, MojoModel):
33
+ """
34
+ Full custom user model.
35
+ """
36
+ created = models.DateTimeField(auto_now_add=True, editable=False)
37
+ modified = models.DateTimeField(auto_now_add=True, editable=True)
38
+ last_activity = models.DateTimeField(default=None, null=True, db_index=True)
39
+
40
+ uuid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True)
41
+ username = models.TextField(unique=True)
42
+ email = models.EmailField(unique=True)
43
+ phone_number = models.CharField(max_length=32, blank=True, null=True, default=None)
44
+ is_active = models.BooleanField(default=True, db_index=True)
45
+ display_name = models.CharField(max_length=80, blank=True, null=True, default=None)
46
+ # key used for sessions and general authentication algs
47
+ auth_key = models.TextField(null=True, default=None)
48
+ onetime_code = models.TextField(null=True, default=None)
49
+ # JSON-based permissions field
50
+ permissions = models.JSONField(default=dict, blank=True)
51
+ # JSON-based metadata field
52
+ metadata = models.JSONField(default=dict, blank=True)
53
+
54
+ # required default fields
55
+ first_name = models.CharField(max_length=80, default="")
56
+ last_name = models.CharField(max_length=80, default="")
57
+ is_active = models.BooleanField(default=True)
58
+ is_staff = models.BooleanField(default=False) # Required for admin access
59
+ is_superuser = models.BooleanField(default=False)
60
+ date_joined = models.DateTimeField(auto_now_add=True)
61
+
62
+ is_email_verified = models.BooleanField(default=False)
63
+ is_phone_verified = models.BooleanField(default=False)
64
+
65
+ USERNAME_FIELD = 'username'
66
+ objects = CustomUserManager()
67
+
68
+ class RestMeta:
69
+ NO_SHOW_FIELDS = ["password", "auth_key", "onetime_code"]
70
+ SEARCH_FIELDS = ["username", "email", "display_name"]
71
+ VIEW_PERMS = ["view_users", "manage_users", "owner"]
72
+ SAVE_PERMS = ["manage_users", "owner"]
73
+ LIST_DEFAULT_FILTERS = {
74
+ "is_active": True
75
+ }
76
+ UNIQUE_LOOKUP = ["username", "email"]
77
+ GRAPHS = {
78
+ "basic": {
79
+ "fields": [
80
+ 'id',
81
+ 'display_name',
82
+ 'username',
83
+ 'email',
84
+ 'phone_number',
85
+ 'last_login',
86
+ 'last_activity',
87
+ 'is_active'
88
+ ]
89
+ },
90
+ "default": {
91
+ "fields": [
92
+ 'id',
93
+ 'display_name',
94
+ 'username',
95
+ 'email',
96
+ 'phone_number',
97
+ 'last_login',
98
+ 'last_activity',
99
+ 'permissions',
100
+ 'metadata',
101
+ 'is_active'
102
+ ],
103
+ },
104
+ }
105
+
106
+ def __str__(self):
107
+ return self.email
108
+
109
+ def is_request_user(self, request=None):
110
+ if request is None:
111
+ request = self.active_request
112
+ if request is None:
113
+ return False
114
+ return request.user.id == self.id
115
+
116
+ def touch(self):
117
+ # can't subtract offset-naive and offset-aware datetimes
118
+ if self.last_activity is None or dates.has_time_elsapsed(self.last_activity, seconds=USER_LAST_ACTIVITY_FREQ):
119
+ self.last_activity = dates.utcnow()
120
+ self.atomic_save()
121
+
122
+ def get_auth_key(self):
123
+ if self.auth_key is None:
124
+ self.auth_key = uuid.uuid4().hex
125
+ self.atomic_save()
126
+ return self.auth_key
127
+
128
+ def set_permissions(self, value, request):
129
+ if not isinstance(value, dict):
130
+ return
131
+ for key in value:
132
+ if key in USER_PERMS_PROTECTION:
133
+ if not request.user.has_permission(USER_PERMS_PROTECTION[key]):
134
+ raise merrors.PermissionDeniedException()
135
+ elif not request.user.has_permission("manage_users"):
136
+ raise merrors.PermissionDeniedException()
137
+ if bool(value[key]):
138
+ self.add_permission(key)
139
+ else:
140
+ self.remove_permission(key)
141
+
142
+ def has_module_perms(self, app_label):
143
+ """Check if user has any permissions in a given app."""
144
+ return True # Or customize based on your `permissions` JSON
145
+
146
+ def has_permission(self, perm_key):
147
+ """Check if user has a specific permission in JSON field."""
148
+ if isinstance(perm_key, list):
149
+ for pk in perm_key:
150
+ if self.has_permission(pk):
151
+ return True
152
+ return False
153
+ if perm_key == "all":
154
+ return True
155
+ return self.permissions.get(perm_key, False)
156
+
157
+ def add_permission(self, perm_key, value=True):
158
+ """Dynamically add a permission."""
159
+ if isinstance(perm_key, (list, set)):
160
+ for pk in perm_key:
161
+ self.permissions[pk] = value
162
+ else:
163
+ self.permissions[perm_key] = value
164
+ self.save()
165
+
166
+ def remove_permission(self, perm_key):
167
+ """Remove a permission."""
168
+ if isinstance(perm_key, (list, set)):
169
+ for pk in perm_key:
170
+ if pk in self.permissions:
171
+ del self.permissions[pk]
172
+ else:
173
+ if perm_key in self.permissions:
174
+ del self.permissions[perm_key]
175
+ self.save()
176
+
177
+ def remove_all_permissions(self):
178
+ self.permissions = {}
179
+ self.save()
180
+
181
+ def save_password(self, value):
182
+ self.set_password(value)
183
+ self.save()
184
+
185
+ def save(self, *args, **kwargs):
186
+ if not self.username:
187
+ self.username = self.email.split("@")[0]
188
+ if not self.display_name:
189
+ self.display_name = self.username
190
+ super().save(*args, **kwargs)
191
+
192
+ def check_edit_permission(self, perms, request):
193
+ if "owner" in perms and self.is_request_user():
194
+ return True
195
+ return request.user.has_permission(perms)
196
+
197
+ @classmethod
198
+ def validate_jwt(cls, token):
199
+ token_manager = JWToken()
200
+ jwt_data = token_manager.decode(token, validate=False)
201
+ if jwt_data.uid is None:
202
+ return None, "Invalid token data"
203
+ user = User.objects.filter(id=jwt_data.uid).last()
204
+ if user is None:
205
+ return None, "Invalid token user"
206
+ token_manager.key = user.auth_key
207
+ if not token_manager.is_token_valid(token):
208
+ if token_manager.is_expired:
209
+ return user, "Token expired"
210
+ return user, "Token has invalid signature"
211
+ return user, None
@@ -0,0 +1,3 @@
1
+ APP_NAME = ""
2
+ from .user import *
3
+ from .group import *
@@ -0,0 +1,25 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps.account.models import Group, GroupMember
3
+
4
+
5
+ @md.URL('group')
6
+ @md.URL('group/<int:pk>')
7
+ def on_group(request, pk=None):
8
+ return Group.on_rest_request(request, pk)
9
+
10
+
11
+ @md.URL('group/member')
12
+ @md.URL('group/member/<int:pk>')
13
+ def on_group_member(request, pk=None):
14
+ return GroupMember.on_rest_request(request, pk)
15
+
16
+
17
+ @md.GET('group/<int:pk>/member')
18
+ def on_group_me_member(request, pk=None):
19
+ request.group = Group.objects.filter(pk=pk).last()
20
+ if request.group is None:
21
+ return Group.rest_error_response(request, 403, error="GET permission denied: Group")
22
+ member = request.group.get_member_for_user(request.user)
23
+ if member is None:
24
+ return Group.rest_error_response(request, 403, error="GET permission denied: Member")
25
+ return member.on_rest_get(request)
@@ -0,0 +1,47 @@
1
+ from mojo import decorators as md
2
+ from mojo.apps.account.utils.jwtoken import JWToken
3
+ # from django.http import JsonResponse
4
+ from mojo.helpers.response import JsonResponse
5
+ from mojo.apps.account.models.user import User
6
+ import datetime
7
+
8
+ @md.URL('user')
9
+ @md.URL('user/<int:pk>')
10
+ def on_user(request, pk=None):
11
+ return User.on_rest_request(request, pk)
12
+
13
+
14
+ @md.GET('user/me')
15
+ def on_user_me(request):
16
+ return User.on_rest_request(request, request.user.pk)
17
+
18
+
19
+ @md.POST('refresh_token')
20
+ @md.requires_params("refresh_token")
21
+ def on_refresh_token(request):
22
+ user, error = User.validate_jwt(request.DATA.refresh_token)
23
+ if error is not None:
24
+ return JsonResponse({'error': error}, status=401)
25
+ # future look at keeping the refresh token the same but updating the access_token
26
+ # TODO add device id to the token as well
27
+ user.touch()
28
+ token_package = JWToken(user.get_auth_key()).create(uid=user.id)
29
+ return JsonResponse(dict(status=True, data=token_package))
30
+
31
+
32
+ @md.POST("login")
33
+ @md.requires_params("username", "password")
34
+ def on_user_login(request):
35
+ username = request.DATA.username
36
+ password = request.DATA.password
37
+ user = User.objects.filter(username=username.lower().strip()).last()
38
+ if user is None:
39
+ return JsonResponse(dict(status=False, error="Invalid username or password", code=403))
40
+ if not user.check_password(password):
41
+ # Authentication successful
42
+ user.report_incident(f"{user.username} enter an invalid password", "invalid_password")
43
+ return JsonResponse(dict(status=False, error="Invalid username or password", code=401))
44
+ user.last_login = datetime.datetime.utcnow()
45
+ user.touch()
46
+ token_package = JWToken(user.get_auth_key()).create(uid=user.id)
47
+ return JsonResponse(dict(status=True, data=token_package))
File without changes
@@ -0,0 +1,72 @@
1
+ import jwt
2
+ import datetime
3
+ import time
4
+ from objict import objict
5
+
6
+ class JWToken:
7
+ def __init__(self, key=f"{time.time()}", access_token_expiry=21600, refresh_token_expiry=604800, alg="HS256", token=None):
8
+ self.key = key
9
+ self.access_token_expiry = access_token_expiry
10
+ self.refresh_token_expiry = refresh_token_expiry
11
+ self.alg = alg
12
+ self.is_expired = False
13
+ self.invalid_sig = False
14
+ self.is_valid = False
15
+ self.payload = None
16
+ if token is not None:
17
+ self.is_valid, self.payload = self.decode(token)
18
+
19
+ def decode(self, token, validate=True):
20
+ payload = objict.fromdict(jwt.decode(token, self.key, algorithms=self.alg, options={"verify_signature":False}))
21
+ if not validate:
22
+ return payload
23
+ is_valid = self.is_token_valid(token)
24
+ return is_valid, payload
25
+
26
+ def create(self, **kwargs):
27
+ package = objict()
28
+ package.access_token = self.create_access_token(**kwargs)
29
+ package.refresh_token = self.create_access_token(**kwargs)
30
+ return package
31
+
32
+ def create_access_token(self, **kwargs):
33
+ payload = dict(kwargs)
34
+ payload['exp'] = self._get_exp_time(self.access_token_expiry)
35
+ payload['token_type'] = "access"
36
+ payload["iat"] = int(time.time())
37
+ token = jwt.encode(payload, self.key, algorithm=self.alg)
38
+ return token
39
+
40
+ def create_refresh_token(self, **kwargs):
41
+ payload = dict(kwargs)
42
+ payload['exp'] = self._get_exp_time(self.refresh_token_expiry)
43
+ payload['token_type'] = "refresh"
44
+ payload["iat"] = int(time.time())
45
+ token = jwt.encode(payload, self.key, algorithm=self.alg)
46
+ return token
47
+
48
+ def refresh_access_token(self, refresh_token):
49
+ try:
50
+ decoded = jwt.decode(refresh_token, self.key, algorithms=[self.alg])
51
+ new_access_token = self.create_access_token(**decoded)
52
+ return new_access_token
53
+ except jwt.ExpiredSignatureError:
54
+ raise Exception("Refresh token has expired.")
55
+ except jwt.InvalidTokenError:
56
+ raise Exception("Invalid refresh token.")
57
+
58
+ def _get_exp_time(self, expiry_seconds):
59
+ return datetime.datetime.utcnow() + datetime.timedelta(seconds=expiry_seconds)
60
+
61
+ def is_token_valid(self, token):
62
+ try:
63
+ self.is_expired = False
64
+ self.invalid_sig = False
65
+ jwt.decode(token, self.key, algorithms=['HS256'])
66
+ return True
67
+ except jwt.ExpiredSignatureError:
68
+ self.is_expired = True
69
+ return False
70
+ except jwt.InvalidTokenError:
71
+ self.invalid_sig = True
72
+ return False
@@ -0,0 +1,54 @@
1
+ from fido2.webauthn import PublicKeyCredentialRpEntity, AttestedCredentialData, ResidentKeyRequirement
2
+ from fido2.server import Fido2Server
3
+ from fido2.utils import websafe_decode, websafe_encode
4
+ from objict import objict
5
+ from mojo.helpers.settings import settings
6
+
7
+
8
+ class PasskeyAuthenticator:
9
+ def __init__(self, rp_id=settings.PASSKEYS_RP_ID, rp_name=settings.PASSKEYS_RP_NAME):
10
+ self.server = Fido2Server(PublicKeyCredentialRpEntity(id=rp_id, name=rp_name))
11
+
12
+ def register_begin(self, member, attachment="cross-platform"):
13
+ request = {
14
+ "id": member.uuid,
15
+ "name": member.username,
16
+ "displayName": member.display_name
17
+ }
18
+ data, state = self.server.register_begin(
19
+ request,
20
+ authenticator_attachment=attachment,
21
+ resident_key_requirement=ResidentKeyRequirement.PREFERRED)
22
+ response = objict(state=state, data=objict.fromdict(dict(data)), rp=objict(self.server.rp))
23
+ response.excludeCredentials = self.exclude_credentials(member, websafe=True)
24
+ return response
25
+
26
+ def register_complete(self, credentials, fido2_state):
27
+ auth_data = self.server.register_complete(
28
+ fido2_state,
29
+ response=credentials
30
+ )
31
+ return websafe_encode(auth_data.credential_data)
32
+
33
+ def exclude_credentials(self, member, websafe=False):
34
+ creds = [AttestedCredentialData(websafe_decode(uk.token)) for uk in member.passkeys.all()]
35
+ if websafe:
36
+ return [dict(type="public-key", id=websafe_encode(acd.credential_id)) for acd in creds]
37
+ return creds
38
+
39
+ def authenticate_begin(self, member):
40
+ creds = [AttestedCredentialData(websafe_decode(uk.token)) for uk in member.passkeys.all()]
41
+ challenge, state = self.server.authenticate_begin(creds)
42
+ response = objict(state=state, challenge=challenge, rp=objict(self.server.rp))
43
+ return response
44
+
45
+ def authenticate_complete(self, credential, public_key, fido2_state):
46
+ stored_credentials = [AttestedCredentialData(websafe_decode(public_key))]
47
+ try:
48
+ self.server.authenticate_complete(
49
+ fido2_state,
50
+ credentials=stored_credentials,
51
+ response=credential)
52
+ return True
53
+ except Exception:
54
+ return False