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.
- django_nativemojo-0.1.10.dist-info/LICENSE +19 -0
- django_nativemojo-0.1.10.dist-info/METADATA +96 -0
- django_nativemojo-0.1.10.dist-info/NOTICE +8 -0
- django_nativemojo-0.1.10.dist-info/RECORD +194 -0
- django_nativemojo-0.1.10.dist-info/WHEEL +4 -0
- mojo/__init__.py +3 -0
- mojo/apps/account/__init__.py +1 -0
- mojo/apps/account/admin.py +91 -0
- mojo/apps/account/apps.py +16 -0
- mojo/apps/account/migrations/0001_initial.py +77 -0
- mojo/apps/account/migrations/0002_user_is_email_verified_user_is_phone_verified.py +23 -0
- mojo/apps/account/migrations/0003_group_mojo_secrets_user_mojo_secrets.py +23 -0
- mojo/apps/account/migrations/__init__.py +0 -0
- mojo/apps/account/models/__init__.py +3 -0
- mojo/apps/account/models/group.py +98 -0
- mojo/apps/account/models/member.py +95 -0
- mojo/apps/account/models/pkey.py +18 -0
- mojo/apps/account/models/user.py +211 -0
- mojo/apps/account/rest/__init__.py +3 -0
- mojo/apps/account/rest/group.py +25 -0
- mojo/apps/account/rest/user.py +47 -0
- mojo/apps/account/utils/__init__.py +0 -0
- mojo/apps/account/utils/jwtoken.py +72 -0
- mojo/apps/account/utils/passkeys.py +54 -0
- mojo/apps/fileman/README.md +549 -0
- mojo/apps/fileman/__init__.py +0 -0
- mojo/apps/fileman/apps.py +15 -0
- mojo/apps/fileman/backends/__init__.py +117 -0
- mojo/apps/fileman/backends/base.py +319 -0
- mojo/apps/fileman/backends/filesystem.py +397 -0
- mojo/apps/fileman/backends/s3.py +398 -0
- mojo/apps/fileman/examples/configurations.py +378 -0
- mojo/apps/fileman/examples/usage_example.py +665 -0
- mojo/apps/fileman/management/__init__.py +1 -0
- mojo/apps/fileman/management/commands/__init__.py +1 -0
- mojo/apps/fileman/management/commands/cleanup_expired_uploads.py +222 -0
- mojo/apps/fileman/models/__init__.py +7 -0
- mojo/apps/fileman/models/file.py +292 -0
- mojo/apps/fileman/models/manager.py +227 -0
- mojo/apps/fileman/models/render.py +0 -0
- mojo/apps/fileman/rest/__init__ +0 -0
- mojo/apps/fileman/rest/__init__.py +23 -0
- mojo/apps/fileman/rest/fileman.py +13 -0
- mojo/apps/fileman/rest/upload.py +92 -0
- mojo/apps/fileman/utils/__init__.py +19 -0
- mojo/apps/fileman/utils/upload.py +616 -0
- mojo/apps/incident/__init__.py +1 -0
- mojo/apps/incident/handlers/__init__.py +3 -0
- mojo/apps/incident/handlers/event_handlers.py +142 -0
- mojo/apps/incident/migrations/0001_initial.py +83 -0
- mojo/apps/incident/migrations/0002_rename_bundle_ruleset_bundle_minutes_event_hostname_and_more.py +44 -0
- mojo/apps/incident/migrations/0003_alter_event_model_id.py +18 -0
- mojo/apps/incident/migrations/0004_alter_incident_model_id.py +18 -0
- mojo/apps/incident/migrations/__init__.py +0 -0
- mojo/apps/incident/models/__init__.py +3 -0
- mojo/apps/incident/models/event.py +135 -0
- mojo/apps/incident/models/incident.py +33 -0
- mojo/apps/incident/models/rule.py +247 -0
- mojo/apps/incident/parsers/__init__.py +0 -0
- mojo/apps/incident/parsers/ossec/__init__.py +1 -0
- mojo/apps/incident/parsers/ossec/core.py +82 -0
- mojo/apps/incident/parsers/ossec/parsed.py +23 -0
- mojo/apps/incident/parsers/ossec/rules.py +124 -0
- mojo/apps/incident/parsers/ossec/utils.py +169 -0
- mojo/apps/incident/reporter.py +42 -0
- mojo/apps/incident/rest/__init__.py +2 -0
- mojo/apps/incident/rest/event.py +23 -0
- mojo/apps/incident/rest/ossec.py +22 -0
- mojo/apps/logit/__init__.py +0 -0
- mojo/apps/logit/admin.py +37 -0
- mojo/apps/logit/migrations/0001_initial.py +32 -0
- mojo/apps/logit/migrations/0002_log_duid_log_payload_log_username.py +28 -0
- mojo/apps/logit/migrations/0003_log_level.py +18 -0
- mojo/apps/logit/migrations/__init__.py +0 -0
- mojo/apps/logit/models/__init__.py +1 -0
- mojo/apps/logit/models/log.py +57 -0
- mojo/apps/logit/rest.py +9 -0
- mojo/apps/metrics/README.md +79 -0
- mojo/apps/metrics/__init__.py +12 -0
- mojo/apps/metrics/redis_metrics.py +331 -0
- mojo/apps/metrics/rest/__init__.py +1 -0
- mojo/apps/metrics/rest/base.py +152 -0
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/metrics/utils.py +227 -0
- mojo/apps/notify/README.md +91 -0
- mojo/apps/notify/README_NOTIFICATIONS.md +566 -0
- mojo/apps/notify/__init__.py +0 -0
- mojo/apps/notify/admin.py +52 -0
- mojo/apps/notify/handlers/__init__.py +0 -0
- mojo/apps/notify/handlers/example_handlers.py +516 -0
- mojo/apps/notify/handlers/ses/__init__.py +25 -0
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +25 -0
- mojo/apps/notify/handlers/ses/message.py +86 -0
- mojo/apps/notify/management/__init__.py +0 -0
- mojo/apps/notify/management/commands/__init__.py +1 -0
- mojo/apps/notify/management/commands/process_notifications.py +370 -0
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +12 -0
- mojo/apps/notify/models/account.py +128 -0
- mojo/apps/notify/models/attachment.py +24 -0
- mojo/apps/notify/models/bounce.py +68 -0
- mojo/apps/notify/models/complaint.py +40 -0
- mojo/apps/notify/models/inbox.py +113 -0
- mojo/apps/notify/models/inbox_message.py +173 -0
- mojo/apps/notify/models/outbox.py +129 -0
- mojo/apps/notify/models/outbox_message.py +288 -0
- mojo/apps/notify/models/template.py +30 -0
- mojo/apps/notify/providers/__init__.py +0 -0
- mojo/apps/notify/providers/aws.py +73 -0
- mojo/apps/notify/rest/__init__.py +0 -0
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +2 -0
- mojo/apps/notify/utils/notifications.py +404 -0
- mojo/apps/notify/utils/parsing.py +202 -0
- mojo/apps/notify/utils/render.py +144 -0
- mojo/apps/tasks/README.md +118 -0
- mojo/apps/tasks/__init__.py +11 -0
- mojo/apps/tasks/manager.py +489 -0
- mojo/apps/tasks/rest/__init__.py +2 -0
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +62 -0
- mojo/apps/tasks/runner.py +174 -0
- mojo/apps/tasks/tq_handlers.py +14 -0
- mojo/decorators/__init__.py +3 -0
- mojo/decorators/auth.py +25 -0
- mojo/decorators/cron.py +31 -0
- mojo/decorators/http.py +132 -0
- mojo/decorators/validate.py +14 -0
- mojo/errors.py +88 -0
- mojo/helpers/__init__.py +0 -0
- mojo/helpers/aws/__init__.py +0 -0
- mojo/helpers/aws/client.py +8 -0
- mojo/helpers/aws/s3.py +268 -0
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/cron.py +79 -0
- mojo/helpers/crypto/__init__.py +4 -0
- mojo/helpers/crypto/aes.py +60 -0
- mojo/helpers/crypto/hash.py +59 -0
- mojo/helpers/crypto/privpub/__init__.py +1 -0
- mojo/helpers/crypto/privpub/hybrid.py +97 -0
- mojo/helpers/crypto/privpub/rsa.py +104 -0
- mojo/helpers/crypto/sign.py +36 -0
- mojo/helpers/crypto/too.l.py +25 -0
- mojo/helpers/crypto/utils.py +26 -0
- mojo/helpers/daemon.py +94 -0
- mojo/helpers/dates.py +69 -0
- mojo/helpers/dns/__init__.py +0 -0
- mojo/helpers/dns/godaddy.py +62 -0
- mojo/helpers/filetypes.py +128 -0
- mojo/helpers/logit.py +310 -0
- mojo/helpers/modules.py +95 -0
- mojo/helpers/paths.py +63 -0
- mojo/helpers/redis.py +10 -0
- mojo/helpers/request.py +89 -0
- mojo/helpers/request_parser.py +269 -0
- mojo/helpers/response.py +14 -0
- mojo/helpers/settings.py +146 -0
- mojo/helpers/sysinfo.py +140 -0
- mojo/helpers/ua.py +0 -0
- mojo/middleware/__init__.py +0 -0
- mojo/middleware/auth.py +26 -0
- mojo/middleware/logging.py +55 -0
- mojo/middleware/mojo.py +21 -0
- mojo/migrations/0001_initial.py +32 -0
- mojo/migrations/__init__.py +0 -0
- mojo/models/__init__.py +2 -0
- mojo/models/meta.py +262 -0
- mojo/models/rest.py +538 -0
- mojo/models/secrets.py +59 -0
- mojo/rest/__init__.py +1 -0
- mojo/rest/info.py +26 -0
- mojo/serializers/__init__.py +0 -0
- mojo/serializers/models.py +165 -0
- mojo/serializers/openapi.py +188 -0
- mojo/urls.py +38 -0
- mojo/ws4redis/README.md +174 -0
- mojo/ws4redis/__init__.py +2 -0
- mojo/ws4redis/client.py +283 -0
- mojo/ws4redis/connection.py +327 -0
- mojo/ws4redis/exceptions.py +32 -0
- mojo/ws4redis/redis.py +183 -0
- mojo/ws4redis/servers/__init__.py +0 -0
- mojo/ws4redis/servers/base.py +86 -0
- mojo/ws4redis/servers/django.py +171 -0
- mojo/ws4redis/servers/uwsgi.py +63 -0
- mojo/ws4redis/settings.py +45 -0
- mojo/ws4redis/utf8validator.py +128 -0
- mojo/ws4redis/websocket.py +403 -0
- testit/__init__.py +0 -0
- testit/client.py +147 -0
- testit/faker.py +20 -0
- testit/helpers.py +198 -0
- 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,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
|