amsdal 0.3.3__cp311-cp311-macosx_10_9_universal2.whl → 0.5.29__cp311-cp311-macosx_10_9_universal2.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.
- amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +56 -2
- amsdal/__about__.py +1 -1
- amsdal/__init__.py +20 -0
- amsdal/__init__.pyi +9 -0
- amsdal/__migrations__/0000_initial.py +23 -190
- amsdal/__migrations__/0001_create_class_file.py +61 -0
- amsdal/__migrations__/0002_create_class_file.py +109 -0
- amsdal/__migrations__/0003_update_class_file.py +91 -0
- amsdal/__migrations__/0004_update_class_file.py +45 -0
- amsdal/cloud/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/client.cpython-311-darwin.so +0 -0
- amsdal/cloud/constants.cpython-311-darwin.so +0 -0
- amsdal/cloud/enums.cpython-311-darwin.so +0 -0
- amsdal/cloud/models/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/models/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_basic_auth.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_dependency.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_secret.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_env.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_session.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_basic_auth.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_dependency.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_env.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_secret.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/destroy_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/expose_db.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/get_monitoring_info.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_dependencies.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_deploys.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_envs.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_secrets.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/manager.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/signup_action.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/update_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.pyi +0 -1
- amsdal/cloud/services/auth/manager.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/signup_service.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/token.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/token.pyi +0 -1
- amsdal/configs/main.py +40 -20
- amsdal/configs/main.pyi +19 -18
- amsdal/contrib/__init__.cpython-311-darwin.so +0 -0
- amsdal/contrib/auth/errors.py +36 -0
- amsdal/contrib/auth/errors.pyi +12 -0
- amsdal/contrib/auth/fixtures/basic_permissions.json +64 -0
- amsdal/contrib/auth/lifecycle/consumer.py +13 -13
- amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
- amsdal/contrib/auth/migrations/0000_initial.py +69 -31
- amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
- amsdal/contrib/auth/models/__init__.py +1 -0
- amsdal/contrib/auth/models/backup_code.py +85 -0
- amsdal/contrib/auth/models/email_mfa_device.py +108 -0
- amsdal/contrib/auth/models/login_session.py +235 -0
- amsdal/contrib/auth/models/mfa_device.py +86 -0
- amsdal/contrib/auth/models/permission.py +23 -0
- amsdal/contrib/auth/models/sms_device.py +113 -0
- amsdal/contrib/auth/models/totp_device.py +58 -0
- amsdal/contrib/auth/models/user.py +156 -0
- amsdal/contrib/auth/services/__init__.py +1 -0
- amsdal/contrib/auth/services/mfa_device_service.py +544 -0
- amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
- amsdal/contrib/auth/services/totp_service.py +358 -0
- amsdal/contrib/auth/services/totp_service.pyi +158 -0
- amsdal/contrib/auth/settings.py +8 -0
- amsdal/contrib/auth/settings.pyi +8 -0
- amsdal/contrib/auth/transactions/__init__.py +1 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.py +463 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
- amsdal/contrib/auth/transactions/totp_transactions.py +206 -0
- amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
- amsdal/contrib/auth/utils/__init__.py +0 -0
- amsdal/contrib/auth/utils/__init__.pyi +0 -0
- amsdal/contrib/auth/utils/mfa.py +257 -0
- amsdal/contrib/auth/utils/mfa.pyi +119 -0
- amsdal/contrib/frontend_configs/conversion/convert.py +85 -25
- amsdal/contrib/frontend_configs/conversion/convert.pyi +0 -1
- amsdal/contrib/frontend_configs/lifecycle/consumer.py +32 -13
- amsdal/contrib/frontend_configs/lifecycle/consumer.pyi +1 -1
- amsdal/contrib/frontend_configs/migrations/0000_initial.py +167 -195
- amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
- amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
- amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
- amsdal/contrib/frontend_configs/models/__init__.py +0 -0
- amsdal/contrib/frontend_configs/models/frontend_activator_config.py +22 -0
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator.py +11 -0
- amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +110 -0
- amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator.py +21 -0
- amsdal/contrib/frontend_configs/models/frontend_config_option.py +12 -0
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.py +17 -0
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option.py +13 -0
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask.py +14 -0
- amsdal/contrib/frontend_configs/models/frontend_config_validator.py +28 -0
- amsdal/contrib/frontend_configs/models/frontend_control_config.py +110 -0
- amsdal/contrib/frontend_configs/models/frontend_model_config.py +14 -0
- amsdal/errors.py +0 -3
- amsdal/errors.pyi +0 -1
- amsdal/fixtures/__init__.cpython-311-darwin.so +0 -0
- amsdal/fixtures/manager.cpython-311-darwin.so +0 -0
- amsdal/fixtures/manager.pyi +73 -123
- amsdal/fixtures/utils.cpython-311-darwin.so +0 -0
- amsdal/fixtures/utils.pyi +9 -0
- amsdal/manager.cpython-311-darwin.so +0 -0
- amsdal/manager.pyi +9 -96
- amsdal/mixins/__init__.cpython-311-darwin.so +0 -0
- amsdal/mixins/class_versions_mixin.cpython-311-darwin.so +0 -0
- amsdal/models/__init__.py +19 -0
- amsdal/models/core/__init__.py +0 -0
- amsdal/models/core/class_object.py +38 -0
- amsdal/models/core/class_property.py +26 -0
- amsdal/models/core/file.py +243 -0
- amsdal/models/core/fixture.py +25 -0
- amsdal/models/core/option.py +11 -0
- amsdal/models/core/storage_metadata.py +15 -0
- amsdal/models/core/validator.py +12 -0
- amsdal/models/mixins.py +31 -0
- amsdal/models/types/__init__.py +0 -0
- amsdal/models/types/object.py +26 -0
- amsdal/queryset/__init__.py +21 -0
- amsdal/queryset/__init__.pyi +6 -0
- amsdal/schemas/core/class_object/model.json +20 -0
- amsdal/schemas/core/class_property/model.json +19 -0
- amsdal/schemas/core/file/properties/from_file.py +1 -1
- amsdal/schemas/core/file/properties/validate_data.py +3 -4
- amsdal/schemas/core/storage_metadata/model.json +52 -0
- amsdal/schemas/interfaces.py +25 -0
- amsdal/schemas/interfaces.pyi +20 -0
- amsdal/schemas/manager.cpython-311-darwin.so +0 -0
- amsdal/schemas/manager.py +0 -116
- amsdal/schemas/manager.pyi +0 -65
- amsdal/schemas/mixins/__init__.py +0 -0
- amsdal/schemas/mixins/__init__.pyi +0 -0
- amsdal/schemas/mixins/check_dependencies_mixin.py +130 -0
- amsdal/schemas/mixins/check_dependencies_mixin.pyi +45 -0
- amsdal/schemas/mixins/verify_schemas_mixin.py +96 -0
- amsdal/schemas/mixins/verify_schemas_mixin.pyi +33 -0
- amsdal/schemas/repository.py +84 -0
- amsdal/schemas/repository.pyi +22 -0
- amsdal/schemas/utils.py +16 -0
- amsdal/schemas/utils.pyi +10 -0
- amsdal/services/__init__.py +11 -0
- amsdal/services/__init__.pyi +4 -0
- amsdal/services/external_connections.py +262 -0
- amsdal/services/external_connections.pyi +190 -0
- amsdal/services/external_model_generator.py +350 -0
- amsdal/services/external_model_generator.pyi +134 -0
- amsdal/services/transaction_execution.cpython-311-darwin.so +0 -0
- amsdal/services/transaction_execution.pyi +2 -1
- amsdal/storages/__init__.py +20 -0
- amsdal/storages/__init__.pyi +8 -0
- amsdal/storages/file_system.py +214 -0
- amsdal/storages/file_system.pyi +36 -0
- amsdal/transactions/__init__.py +13 -0
- amsdal/transactions/__init__.pyi +4 -0
- amsdal/utils/rollback/__init__.py +99 -54
- amsdal/utils/rollback/__init__.pyi +6 -0
- amsdal/utils/tests/enums.py +0 -2
- amsdal/utils/tests/helpers.py +253 -231
- amsdal/utils/tests/migrations.py +157 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/METADATA +17 -10
- amsdal-0.5.29.dist-info/RECORD +276 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/WHEEL +1 -1
- amsdal/__migrations__/0001_datetime_type.py +0 -18
- amsdal/__migrations__/0002_fixture_order.py +0 -40
- amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -56
- amsdal/contrib/auth/models/login_session/hooks/pre_init.py +0 -68
- amsdal/contrib/auth/models/login_session/model.json +0 -23
- amsdal/contrib/auth/models/login_session/modifiers/display_name.py +0 -11
- amsdal/contrib/auth/models/permission/fixtures/basic_permissions.json +0 -62
- amsdal/contrib/auth/models/permission/model.json +0 -18
- amsdal/contrib/auth/models/permission/modifiers/display_name.py +0 -11
- amsdal/contrib/auth/models/user/hooks/post_init.py +0 -76
- amsdal/contrib/auth/models/user/hooks/pre_create.py +0 -8
- amsdal/contrib/auth/models/user/model.json +0 -25
- amsdal/contrib/auth/models/user/modifiers/display_name.py +0 -19
- amsdal/contrib/frontend_configs/models/frontend_activator_config/model.json +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator/model.json +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator/model.json +0 -52
- amsdal/contrib/frontend_configs/models/frontend_config_option/model.json +0 -15
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base/model.json +0 -6
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base/properties/model_dump.py +0 -13
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option/model.json +0 -19
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask/model.json +0 -26
- amsdal/contrib/frontend_configs/models/frontend_config_validator/model.json +0 -41
- amsdal/contrib/frontend_configs/models/frontend_control_config/model.json +0 -250
- amsdal/contrib/frontend_configs/models/frontend_model_config/fixtures/permissions.json +0 -24
- amsdal/contrib/frontend_configs/models/frontend_model_config/model.json +0 -17
- amsdal/contrib/frontend_configs/models/frontent_config_control_action/model.json +0 -54
- amsdal/contrib/frontend_configs/models/frontent_config_control_action/properties/action_validate.py +0 -33
- amsdal/migration/__init__.cpython-311-darwin.so +0 -0
- amsdal/migration/base_migration_schemas.cpython-311-darwin.so +0 -0
- amsdal/migration/base_migration_schemas.pyi +0 -120
- amsdal/migration/data_classes.cpython-311-darwin.so +0 -0
- amsdal/migration/data_classes.pyi +0 -172
- amsdal/migration/executors/__init__.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/base.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/base.pyi +0 -118
- amsdal/migration/executors/default_executor.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/default_executor.pyi +0 -184
- amsdal/migration/executors/state_executor.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/state_executor.pyi +0 -78
- amsdal/migration/file_migration_executor.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_executor.pyi +0 -68
- amsdal/migration/file_migration_generator.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_generator.pyi +0 -139
- amsdal/migration/file_migration_store.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_store.pyi +0 -61
- amsdal/migration/file_migration_writer.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_writer.pyi +0 -73
- amsdal/migration/migrations.cpython-311-darwin.so +0 -0
- amsdal/migration/migrations.pyi +0 -166
- amsdal/migration/migrations_loader.cpython-311-darwin.so +0 -0
- amsdal/migration/migrations_loader.pyi +0 -32
- amsdal/migration/schemas_loaders.cpython-311-darwin.so +0 -0
- amsdal/migration/schemas_loaders.pyi +0 -37
- amsdal/migration/templates/data_migration.tmpl +0 -18
- amsdal/migration/templates/dict_validator.tmpl +0 -4
- amsdal/migration/templates/migration.tmpl +0 -6
- amsdal/migration/templates/model_class.tmpl +0 -8
- amsdal/migration/templates/model_class_layout.tmpl +0 -24
- amsdal/migration/templates/options_validator.tmpl +0 -4
- amsdal/migration/utils.cpython-311-darwin.so +0 -0
- amsdal/migration/utils.pyi +0 -58
- amsdal/mixins/build_mixin.cpython-311-darwin.so +0 -0
- amsdal/mixins/build_mixin.pyi +0 -78
- amsdal/schemas/core/class_object_meta/model.json +0 -59
- amsdal/schemas/core/class_property_meta/model.json +0 -23
- amsdal/services/__init__.cpython-311-darwin.so +0 -0
- amsdal-0.3.3.dist-info/RECORD +0 -257
- amsdal-0.3.3.dist-info/licenses/LICENSE.txt +0 -107
- /amsdal/{migration → contrib/auth/services}/__init__.pyi +0 -0
- /amsdal/{migration/executors → contrib/auth/transactions}/__init__.pyi +0 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info/licenses}/LICENSE.txt +0 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from datetime import UTC
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
import jwt
|
|
9
|
+
from amsdal_models.classes.model import Model
|
|
10
|
+
from amsdal_utils.models.enums import ModuleType
|
|
11
|
+
from pydantic.fields import Field
|
|
12
|
+
|
|
13
|
+
from amsdal.contrib.auth.utils.mfa import DeviceType
|
|
14
|
+
from amsdal.contrib.auth.utils.mfa import aget_active_user_devices
|
|
15
|
+
from amsdal.contrib.auth.utils.mfa import get_active_user_devices
|
|
16
|
+
|
|
17
|
+
if t.TYPE_CHECKING:
|
|
18
|
+
from amsdal.contrib.auth.models.mfa_device import MFADevice
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LoginSession(Model):
|
|
22
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
23
|
+
email: str = Field(title='Email')
|
|
24
|
+
password: str = Field(title='Password (hash)')
|
|
25
|
+
token: str | None = Field(None, title='Token')
|
|
26
|
+
mfa_code: str | None = Field(None, title='MFA Code')
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def display_name(self) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Returns the display name of the user.
|
|
32
|
+
|
|
33
|
+
This method returns the email of the user as their display name.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: The email of the user.
|
|
37
|
+
"""
|
|
38
|
+
return self.email
|
|
39
|
+
|
|
40
|
+
def pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Pre-initializes a user object by validating email and password, and generating a JWT token.
|
|
43
|
+
|
|
44
|
+
This method checks if the object is new and validates the provided email and password.
|
|
45
|
+
If the email and password are valid, it generates a JWT token and adds it to the kwargs.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
is_new_object (bool): Indicates if the object is new.
|
|
49
|
+
kwargs (dict[str, Any]): The keyword arguments containing user details.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
AuthenticationError: If the email or password is invalid.
|
|
53
|
+
"""
|
|
54
|
+
if not is_new_object or '_metadata' in kwargs:
|
|
55
|
+
return
|
|
56
|
+
from amsdal.contrib.auth.errors import AuthenticationError
|
|
57
|
+
from amsdal.contrib.auth.settings import auth_settings
|
|
58
|
+
|
|
59
|
+
email = kwargs.get('email', None)
|
|
60
|
+
password = kwargs.get('password', None)
|
|
61
|
+
if not email:
|
|
62
|
+
msg = "Email can't be empty"
|
|
63
|
+
raise AuthenticationError(msg)
|
|
64
|
+
if not password:
|
|
65
|
+
msg = "Password can't be empty"
|
|
66
|
+
raise AuthenticationError(msg)
|
|
67
|
+
lowercased_email = email.lower()
|
|
68
|
+
kwargs['email'] = lowercased_email
|
|
69
|
+
|
|
70
|
+
if not auth_settings.AUTH_JWT_KEY:
|
|
71
|
+
msg = 'JWT key is not set'
|
|
72
|
+
raise AuthenticationError(msg)
|
|
73
|
+
|
|
74
|
+
expiration_time = datetime.now(tz=UTC) + timedelta(seconds=auth_settings.AUTH_TOKEN_EXPIRATION)
|
|
75
|
+
token = jwt.encode(
|
|
76
|
+
{'email': lowercased_email, 'exp': expiration_time},
|
|
77
|
+
key=auth_settings.AUTH_JWT_KEY, # type: ignore[arg-type]
|
|
78
|
+
algorithm='HS256',
|
|
79
|
+
)
|
|
80
|
+
kwargs['token'] = token
|
|
81
|
+
|
|
82
|
+
def pre_create(self) -> None:
|
|
83
|
+
import bcrypt
|
|
84
|
+
|
|
85
|
+
from amsdal.contrib.auth.errors import AuthenticationError
|
|
86
|
+
from amsdal.contrib.auth.errors import InvalidMFACodeError
|
|
87
|
+
from amsdal.contrib.auth.errors import MFARequiredError
|
|
88
|
+
from amsdal.contrib.auth.models.user import User
|
|
89
|
+
|
|
90
|
+
user = User.objects.filter(email=self.email).latest().first().execute()
|
|
91
|
+
|
|
92
|
+
if not user:
|
|
93
|
+
msg = 'User not found'
|
|
94
|
+
raise AuthenticationError(msg)
|
|
95
|
+
|
|
96
|
+
if not bcrypt.checkpw(self.password.encode(), user.password):
|
|
97
|
+
msg = 'Invalid password'
|
|
98
|
+
raise AuthenticationError(msg)
|
|
99
|
+
|
|
100
|
+
devices = get_active_user_devices(user)
|
|
101
|
+
if any(devices.values()):
|
|
102
|
+
if not self.mfa_code:
|
|
103
|
+
msg = 'MFA verification is required. Please provide an MFA code.'
|
|
104
|
+
raise MFARequiredError(msg)
|
|
105
|
+
|
|
106
|
+
# Verify MFA code against user's devices
|
|
107
|
+
if not self._verify_mfa_code(devices, self.mfa_code):
|
|
108
|
+
msg = 'Invalid MFA code'
|
|
109
|
+
raise InvalidMFACodeError(msg)
|
|
110
|
+
|
|
111
|
+
self.password = 'validated'
|
|
112
|
+
|
|
113
|
+
def pre_update(self) -> None:
|
|
114
|
+
from amsdal.contrib.auth.errors import AuthenticationError
|
|
115
|
+
|
|
116
|
+
msg = 'Update not allowed'
|
|
117
|
+
raise AuthenticationError(msg)
|
|
118
|
+
|
|
119
|
+
async def apre_create(self) -> None:
|
|
120
|
+
import bcrypt
|
|
121
|
+
|
|
122
|
+
from amsdal.contrib.auth.errors import AuthenticationError
|
|
123
|
+
from amsdal.contrib.auth.errors import InvalidMFACodeError
|
|
124
|
+
from amsdal.contrib.auth.errors import MFARequiredError
|
|
125
|
+
from amsdal.contrib.auth.models.user import User
|
|
126
|
+
|
|
127
|
+
user = await User.objects.filter(email=self.email).latest().first().aexecute()
|
|
128
|
+
|
|
129
|
+
if not user:
|
|
130
|
+
msg = 'User not found'
|
|
131
|
+
raise AuthenticationError(msg)
|
|
132
|
+
|
|
133
|
+
if not bcrypt.checkpw(self.password.encode(), user.password):
|
|
134
|
+
msg = 'Invalid password'
|
|
135
|
+
raise AuthenticationError(msg)
|
|
136
|
+
|
|
137
|
+
devices = await aget_active_user_devices(user)
|
|
138
|
+
# Check if MFA is required for this user
|
|
139
|
+
if any(devices.values()):
|
|
140
|
+
if not self.mfa_code:
|
|
141
|
+
msg = 'MFA verification is required. Please provide an MFA code.'
|
|
142
|
+
raise MFARequiredError(msg)
|
|
143
|
+
|
|
144
|
+
# Verify MFA code against user's devices
|
|
145
|
+
if not await self._averify_mfa_code(devices, self.mfa_code):
|
|
146
|
+
msg = 'Invalid MFA code'
|
|
147
|
+
raise InvalidMFACodeError(msg)
|
|
148
|
+
|
|
149
|
+
self.password = 'validated'
|
|
150
|
+
|
|
151
|
+
async def apre_update(self) -> None:
|
|
152
|
+
from amsdal.contrib.auth.errors import AuthenticationError
|
|
153
|
+
|
|
154
|
+
msg = 'Update not allowed'
|
|
155
|
+
raise AuthenticationError(msg)
|
|
156
|
+
|
|
157
|
+
def _verify_mfa_code(self, devices: dict[DeviceType, list['MFADevice']], code: str) -> bool: # type: ignore # noqa: F821
|
|
158
|
+
"""
|
|
159
|
+
Verify an MFA code against the user's active devices.
|
|
160
|
+
|
|
161
|
+
This method checks all active and confirmed MFA devices for the user
|
|
162
|
+
and attempts to verify the provided code against each one.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
user: The user attempting to authenticate.
|
|
166
|
+
code: The MFA code to verify.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
bool: True if the code is valid for any device, False otherwise.
|
|
170
|
+
"""
|
|
171
|
+
from datetime import UTC
|
|
172
|
+
from datetime import datetime
|
|
173
|
+
|
|
174
|
+
for device_type, specific_devices in devices.items():
|
|
175
|
+
try:
|
|
176
|
+
for device in specific_devices:
|
|
177
|
+
if device.verify_code(code): # type: ignore[attr-defined]
|
|
178
|
+
# Update last_used_at
|
|
179
|
+
device.last_used_at = datetime.now(tz=UTC) # type: ignore[attr-defined]
|
|
180
|
+
|
|
181
|
+
# Special handling for backup codes (mark as used)
|
|
182
|
+
if device_type == DeviceType.BACKUP_CODE:
|
|
183
|
+
device.mark_as_used() # type: ignore[attr-defined]
|
|
184
|
+
# Special handling for email devices (clear code)
|
|
185
|
+
elif device_type == DeviceType.EMAIL:
|
|
186
|
+
device.clear_code() # type: ignore[attr-defined]
|
|
187
|
+
|
|
188
|
+
device.save() # type: ignore[attr-defined]
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
except Exception: # noqa: S112
|
|
192
|
+
# Continue to next device type if verification fails
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
async def _averify_mfa_code(self, devices: dict[DeviceType, list['MFADevice']], code: str) -> bool: # type: ignore # noqa: F821
|
|
198
|
+
"""
|
|
199
|
+
Verify an MFA code against the user's active devices (async version).
|
|
200
|
+
|
|
201
|
+
This method checks all active and confirmed MFA devices for the user
|
|
202
|
+
and attempts to verify the provided code against each one.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
user: The user attempting to authenticate.
|
|
206
|
+
code: The MFA code to verify.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
bool: True if the code is valid for any device, False otherwise.
|
|
210
|
+
"""
|
|
211
|
+
from datetime import UTC
|
|
212
|
+
from datetime import datetime
|
|
213
|
+
|
|
214
|
+
for device_type, specific_devices in devices.items():
|
|
215
|
+
try:
|
|
216
|
+
for device in specific_devices:
|
|
217
|
+
if device.verify_code(code): # type: ignore[attr-defined]
|
|
218
|
+
# Update last_used_at
|
|
219
|
+
device.last_used_at = datetime.now(tz=UTC) # type: ignore[attr-defined]
|
|
220
|
+
|
|
221
|
+
# Special handling for backup codes (mark as used)
|
|
222
|
+
if device_type == DeviceType.BACKUP_CODE:
|
|
223
|
+
device.mark_as_used() # type: ignore[attr-defined]
|
|
224
|
+
# Special handling for email devices (clear code)
|
|
225
|
+
elif device_type == DeviceType.EMAIL:
|
|
226
|
+
device.clear_code() # type: ignore[attr-defined]
|
|
227
|
+
|
|
228
|
+
await device.asave() # type: ignore[attr-defined]
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
except Exception: # noqa: S112
|
|
232
|
+
# Continue to next device type if verification fails
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
return False
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from datetime import UTC
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from amsdal_models.classes.model import Model
|
|
6
|
+
from amsdal_utils.models.enums import ModuleType
|
|
7
|
+
from pydantic.fields import Field
|
|
8
|
+
|
|
9
|
+
from amsdal.contrib.auth.utils.mfa import DeviceType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _now_utc() -> datetime:
|
|
13
|
+
"""Return current UTC datetime."""
|
|
14
|
+
return datetime.now(tz=UTC)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MFADevice(Model):
|
|
18
|
+
"""
|
|
19
|
+
Base model for Multi-Factor Authentication devices.
|
|
20
|
+
|
|
21
|
+
This model serves as the base class for all MFA device types (TOTP, Backup Codes, Email, SMS).
|
|
22
|
+
Each device is associated with a user and must be confirmed before it can be used for authentication.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
user_email (str): Email of the user who owns this device (reference to User).
|
|
26
|
+
device_type (str): Type of MFA device ('totp', 'backup_code', 'email', 'sms').
|
|
27
|
+
name (str): User-friendly name for the device.
|
|
28
|
+
is_active (bool): Whether the device is currently active and can be used.
|
|
29
|
+
confirmed (bool): Whether the device has been verified during setup.
|
|
30
|
+
created_at (datetime): When the device was created.
|
|
31
|
+
last_used_at (datetime | None): When the device was last used for authentication.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
35
|
+
|
|
36
|
+
user_email: str = Field(title='User Email')
|
|
37
|
+
device_type: DeviceType | None = Field(default=None, title='Device Type')
|
|
38
|
+
name: str = Field(title='Device Name')
|
|
39
|
+
is_active: bool = Field(True, title='Is Active')
|
|
40
|
+
confirmed: bool = Field(False, title='Confirmed')
|
|
41
|
+
created_at: datetime = Field(default_factory=_now_utc, title='Created At')
|
|
42
|
+
last_used_at: datetime | None = Field(None, title='Last Used At')
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def display_name(self) -> str:
|
|
46
|
+
"""
|
|
47
|
+
Returns the display name of the device.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: The device name and type.
|
|
51
|
+
"""
|
|
52
|
+
return f'{self.name} ({self.device_type})'
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return str(self)
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f'MFADevice(name={self.name}, type={self.device_type}, user={self.user_email})'
|
|
59
|
+
|
|
60
|
+
def has_object_permission(self, user: 'User', action: str) -> bool: # type: ignore # noqa: F821
|
|
61
|
+
"""
|
|
62
|
+
Check if a user has permission to perform an action on this device.
|
|
63
|
+
|
|
64
|
+
Users can only manage their own devices. Admins with wildcard permissions
|
|
65
|
+
can manage all devices.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
user: The user requesting the action.
|
|
69
|
+
action: The action being requested (read, update, delete, etc.).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
bool: True if the user has permission, False otherwise.
|
|
73
|
+
"""
|
|
74
|
+
# Users can only manage their own devices
|
|
75
|
+
if self.user_email == user.email:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
# Check if user has admin permissions (wildcard model permissions)
|
|
79
|
+
if user.permissions:
|
|
80
|
+
for permission in user.permissions:
|
|
81
|
+
if permission.model == '*' and permission.action in ('*', action):
|
|
82
|
+
return True
|
|
83
|
+
if permission.model == 'MFADevice' and permission.action in ('*', action):
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
return False
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import ClassVar
|
|
2
|
+
|
|
3
|
+
from amsdal_models.classes.model import Model
|
|
4
|
+
from amsdal_utils.models.enums import ModuleType
|
|
5
|
+
from pydantic.fields import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Permission(Model):
|
|
9
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
10
|
+
model: str = Field(title='Model')
|
|
11
|
+
action: str = Field(title='Action')
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def display_name(self) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Returns the display name of the user.
|
|
17
|
+
|
|
18
|
+
This method returns a formatted string combining the model and action of the user.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
str: The formatted display name in the format 'model:action'.
|
|
22
|
+
"""
|
|
23
|
+
return f'{self.model}:{self.action}'
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from amsdal_utils.models.enums import ModuleType
|
|
6
|
+
from pydantic.fields import Field
|
|
7
|
+
|
|
8
|
+
from amsdal.contrib.auth.models.mfa_device import MFADevice
|
|
9
|
+
from amsdal.contrib.auth.utils.mfa import DeviceType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SMSDevice(MFADevice):
|
|
13
|
+
"""
|
|
14
|
+
SMS-based MFA device model (future implementation).
|
|
15
|
+
|
|
16
|
+
This model represents an SMS-based MFA method where a temporary code is sent
|
|
17
|
+
to the user's phone number for authentication.
|
|
18
|
+
|
|
19
|
+
Note:
|
|
20
|
+
This is a placeholder for future SMS support. Full implementation requires
|
|
21
|
+
integration with an SMS service provider (e.g., Twilio, AWS SNS).
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
phone_number (str): The phone number to send codes to.
|
|
25
|
+
code (str | None): Temporary MFA code (stored temporarily, expires after use).
|
|
26
|
+
code_expires_at (datetime | None): When the current code expires.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
30
|
+
|
|
31
|
+
phone_number: str = Field(title='Phone Number')
|
|
32
|
+
code: str | None = Field(None, title='Current Code')
|
|
33
|
+
code_expires_at: datetime | None = Field(None, title='Code Expiration')
|
|
34
|
+
|
|
35
|
+
def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Post-initializes an SMS MFA device by setting the device type.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
is_new_object (bool): Indicates if the object is new.
|
|
41
|
+
kwargs (dict[str, Any]): The keyword arguments containing device details.
|
|
42
|
+
"""
|
|
43
|
+
super().post_init(is_new_object=is_new_object, kwargs=kwargs)
|
|
44
|
+
self.device_type = DeviceType.SMS
|
|
45
|
+
|
|
46
|
+
def generate_and_send_code(self) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Generate a new MFA code and send it via SMS.
|
|
49
|
+
|
|
50
|
+
This method generates a random numeric code, sets its expiration time,
|
|
51
|
+
and sends it to the configured phone number.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
str: The generated code (for testing purposes).
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
NotImplementedError: This feature requires SMS service integration.
|
|
58
|
+
|
|
59
|
+
Note:
|
|
60
|
+
Full implementation requires integration with an SMS provider.
|
|
61
|
+
Example providers: Twilio, AWS SNS, MessageBird, etc.
|
|
62
|
+
"""
|
|
63
|
+
from amsdal.contrib.auth.utils.mfa import generate_email_mfa_code
|
|
64
|
+
from amsdal.contrib.auth.utils.mfa import get_email_code_expiration
|
|
65
|
+
|
|
66
|
+
# Generate new code
|
|
67
|
+
self.code = generate_email_mfa_code()
|
|
68
|
+
self.code_expires_at = get_email_code_expiration()
|
|
69
|
+
|
|
70
|
+
# TODO: Implement SMS sending with your SMS provider
|
|
71
|
+
# Example with Twilio:
|
|
72
|
+
# from twilio.rest import Client
|
|
73
|
+
# client = Client(account_sid, auth_token)
|
|
74
|
+
# client.messages.create(
|
|
75
|
+
# to=self.phone_number,
|
|
76
|
+
# from_=your_twilio_number,
|
|
77
|
+
# body=f'Your MFA code is: {self.code}'
|
|
78
|
+
# )
|
|
79
|
+
|
|
80
|
+
msg = 'SMS MFA is not yet implemented. Please integrate with an SMS service provider (e.g., Twilio, AWS SNS).'
|
|
81
|
+
raise NotImplementedError(msg)
|
|
82
|
+
|
|
83
|
+
def verify_code(self, code: str) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
Verify an SMS MFA code.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
code (str): The code to verify.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if the code is valid and not expired, False otherwise.
|
|
92
|
+
"""
|
|
93
|
+
from amsdal.contrib.auth.utils.mfa import is_email_code_valid
|
|
94
|
+
|
|
95
|
+
# Check if code matches
|
|
96
|
+
if not self.code or self.code != code:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
# Check if code is expired
|
|
100
|
+
if not self.code_expires_at or not is_email_code_valid(self.code_expires_at):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def clear_code(self) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Clear the current code after successful use or expiration.
|
|
108
|
+
"""
|
|
109
|
+
self.code = None
|
|
110
|
+
self.code_expires_at = None
|
|
111
|
+
|
|
112
|
+
def __str__(self) -> str:
|
|
113
|
+
return f'SMSDevice(name={self.name}, phone={self.phone_number}, user={self.user_email})'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
from amsdal_utils.models.enums import ModuleType
|
|
5
|
+
from pydantic.fields import Field
|
|
6
|
+
|
|
7
|
+
from amsdal.contrib.auth.models.mfa_device import MFADevice
|
|
8
|
+
from amsdal.contrib.auth.utils.mfa import DeviceType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TOTPDevice(MFADevice):
|
|
12
|
+
"""
|
|
13
|
+
Time-based One-Time Password (TOTP) device model.
|
|
14
|
+
|
|
15
|
+
This model represents an authenticator app device (e.g., Google Authenticator, Authy)
|
|
16
|
+
that generates time-based one-time passwords following RFC 6238.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
secret (str): The encrypted TOTP secret key shared with the authenticator app.
|
|
20
|
+
qr_code_url (str | None): URL for the QR code used during device setup.
|
|
21
|
+
digits (int): Number of digits in the generated code (default: 6).
|
|
22
|
+
step (int): Time step in seconds for code generation (default: 30).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
26
|
+
|
|
27
|
+
secret: str = Field(title='TOTP Secret')
|
|
28
|
+
qr_code_url: str | None = Field(None, title='QR Code URL')
|
|
29
|
+
digits: int = Field(6, title='Code Digits')
|
|
30
|
+
step: int = Field(30, title='Time Step (seconds)')
|
|
31
|
+
|
|
32
|
+
def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Post-initializes a TOTP device by setting the device type.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
is_new_object (bool): Indicates if the object is new.
|
|
38
|
+
kwargs (dict[str, Any]): The keyword arguments containing device details.
|
|
39
|
+
"""
|
|
40
|
+
super().post_init(is_new_object=is_new_object, kwargs=kwargs)
|
|
41
|
+
self.device_type = DeviceType.TOTP
|
|
42
|
+
|
|
43
|
+
def verify_code(self, code: str) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Verify a TOTP code against this device's secret.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
code (str): The TOTP code to verify.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
bool: True if the code is valid, False otherwise.
|
|
52
|
+
"""
|
|
53
|
+
from amsdal.contrib.auth.utils.mfa import verify_totp_code
|
|
54
|
+
|
|
55
|
+
return verify_totp_code(self.secret, code, self.digits, self.step)
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f'TOTPDevice(name={self.name}, user={self.user_email})'
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
from amsdal_models.classes.model import Model
|
|
5
|
+
from amsdal_utils.models.enums import ModuleType
|
|
6
|
+
from pydantic.fields import Field
|
|
7
|
+
|
|
8
|
+
from amsdal.contrib.auth.models.permission import * # noqa: F403
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class User(Model):
|
|
12
|
+
__module_type__: ClassVar[ModuleType] = ModuleType.CONTRIB
|
|
13
|
+
email: str = Field(title='Email')
|
|
14
|
+
password: bytes = Field(title='Password (hash)')
|
|
15
|
+
permissions: list['Permission'] | None = Field(None, title='Permissions') # noqa: F405
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return str(self)
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
return f'User(email={self.email})'
|
|
22
|
+
|
|
23
|
+
async def apre_update(self) -> None:
|
|
24
|
+
import bcrypt
|
|
25
|
+
|
|
26
|
+
original_object = await self.arefetch_from_db()
|
|
27
|
+
password = self.password
|
|
28
|
+
if original_object.password and password is not None:
|
|
29
|
+
if isinstance(password, str):
|
|
30
|
+
password = password.encode('utf-8')
|
|
31
|
+
try:
|
|
32
|
+
if not bcrypt.checkpw(password, original_object.password):
|
|
33
|
+
self.password = password
|
|
34
|
+
except ValueError:
|
|
35
|
+
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
|
|
36
|
+
self.password = hashed_password
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def display_name(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Returns the display name of the user.
|
|
42
|
+
|
|
43
|
+
This method returns the email of the user as their display name.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The email of the user.
|
|
47
|
+
"""
|
|
48
|
+
return self.email
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def requires_mfa(self) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Determines if MFA is required for this user.
|
|
54
|
+
|
|
55
|
+
This checks both the per-user override (mfa_required) and the global
|
|
56
|
+
REQUIRE_MFA_BY_DEFAULT setting.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
bool: True if MFA is required, False otherwise.
|
|
60
|
+
"""
|
|
61
|
+
from amsdal.contrib.auth.settings import auth_settings
|
|
62
|
+
|
|
63
|
+
# Fall back to global setting
|
|
64
|
+
return auth_settings.REQUIRE_MFA_BY_DEFAULT
|
|
65
|
+
|
|
66
|
+
async def ahas_valid_mfa_device(self) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Check if the user has at least one confirmed and active MFA device.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
bool: True if the user has a valid MFA device, False otherwise.
|
|
72
|
+
"""
|
|
73
|
+
from amsdal.contrib.auth.utils.mfa import aget_active_user_devices
|
|
74
|
+
|
|
75
|
+
devices = await aget_active_user_devices(self)
|
|
76
|
+
for device_list in devices.values():
|
|
77
|
+
if device_list:
|
|
78
|
+
return True
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def has_valid_mfa_device(self) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
Check if the user has at least one confirmed and active MFA device (sync version).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
bool: True if the user has a valid MFA device, False otherwise.
|
|
87
|
+
"""
|
|
88
|
+
from amsdal.contrib.auth.utils.mfa import get_active_user_devices
|
|
89
|
+
|
|
90
|
+
devices = get_active_user_devices(self)
|
|
91
|
+
for device_list in devices.values():
|
|
92
|
+
if device_list:
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None: # noqa: ARG002
|
|
97
|
+
if 'email' in kwargs and isinstance(kwargs['email'], str):
|
|
98
|
+
kwargs['email'] = kwargs['email'].lower()
|
|
99
|
+
|
|
100
|
+
def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Post-initializes a user object by validating email and password, and hashing the password.
|
|
103
|
+
|
|
104
|
+
This method checks if the email and password are provided and valid. If the object is new,
|
|
105
|
+
it hashes the password and sets the object ID to the lowercased email.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
is_new_object (bool): Indicates if the object is new.
|
|
109
|
+
kwargs (dict[str, Any]): The keyword arguments containing user details.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
UserCreationError: If the email or password is invalid.
|
|
113
|
+
"""
|
|
114
|
+
import bcrypt
|
|
115
|
+
|
|
116
|
+
from amsdal.contrib.auth.errors import UserCreationError
|
|
117
|
+
|
|
118
|
+
email = kwargs.get('email', None)
|
|
119
|
+
password = kwargs.get('password', None)
|
|
120
|
+
if email is None or email == '':
|
|
121
|
+
msg = "Email can't be empty"
|
|
122
|
+
raise UserCreationError(msg)
|
|
123
|
+
if password is None or password == '':
|
|
124
|
+
msg = "Password can't be empty"
|
|
125
|
+
raise UserCreationError(msg)
|
|
126
|
+
kwargs['email'] = email.lower()
|
|
127
|
+
if is_new_object and '_metadata' not in kwargs:
|
|
128
|
+
if isinstance(password, str):
|
|
129
|
+
password = password.encode('utf-8')
|
|
130
|
+
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
|
|
131
|
+
self.password = hashed_password
|
|
132
|
+
self._object_id = email.lower()
|
|
133
|
+
|
|
134
|
+
def pre_create(self) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Pre-creates a user object.
|
|
137
|
+
|
|
138
|
+
This method is a placeholder for any pre-creation logic that needs to be executed
|
|
139
|
+
before a user object is created.
|
|
140
|
+
"""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def pre_update(self) -> None:
|
|
144
|
+
import bcrypt
|
|
145
|
+
|
|
146
|
+
original_object = self.refetch_from_db()
|
|
147
|
+
password = self.password
|
|
148
|
+
if original_object.password and password is not None:
|
|
149
|
+
if isinstance(password, str):
|
|
150
|
+
password = password.encode('utf-8')
|
|
151
|
+
try:
|
|
152
|
+
if not bcrypt.checkpw(password, original_object.password):
|
|
153
|
+
self.password = password
|
|
154
|
+
except ValueError:
|
|
155
|
+
hashed_password = bcrypt.hashpw(password, bcrypt.gensalt())
|
|
156
|
+
self.password = hashed_password
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MFA device management services."""
|