amsdal 0.4.10__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 +28 -0
- amsdal/__about__.py +1 -1
- amsdal/__migrations__/0000_initial.py +22 -203
- 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/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/configs/main.py +17 -1
- amsdal/configs/main.pyi +7 -3
- 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/lifecycle/consumer.py +3 -3
- amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
- amsdal/contrib/auth/migrations/0000_initial.py +55 -52
- 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 +117 -0
- amsdal/contrib/auth/models/mfa_device.py +86 -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 +50 -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/mfa.py +257 -0
- amsdal/contrib/auth/utils/mfa.pyi +119 -0
- amsdal/contrib/frontend_configs/conversion/convert.py +32 -5
- amsdal/contrib/frontend_configs/migrations/0000_initial.py +154 -183
- 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/frontend_config_control_action.py +57 -1
- amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
- amsdal/contrib/frontend_configs/models/frontend_control_config.py +69 -46
- amsdal/fixtures/__init__.cpython-311-darwin.so +0 -0
- amsdal/fixtures/manager.cpython-311-darwin.so +0 -0
- amsdal/fixtures/utils.cpython-311-darwin.so +0 -0
- amsdal/manager.cpython-311-darwin.so +0 -0
- amsdal/manager.pyi +5 -0
- amsdal/mixins/__init__.cpython-311-darwin.so +0 -0
- amsdal/mixins/class_versions_mixin.cpython-311-darwin.so +0 -0
- amsdal/models/core/class_object.py +7 -6
- amsdal/models/core/class_property.py +7 -1
- amsdal/models/core/file.py +168 -81
- amsdal/models/core/storage_metadata.py +15 -0
- amsdal/models/mixins.py +31 -0
- amsdal/models/types/object.py +3 -3
- amsdal/schemas/core/class_object/model.json +20 -0
- amsdal/schemas/core/class_property/model.json +19 -0
- amsdal/schemas/core/file/properties/validate_data.py +2 -3
- amsdal/schemas/core/storage_metadata/model.json +52 -0
- amsdal/schemas/interfaces.pyi +1 -1
- amsdal/schemas/manager.cpython-311-darwin.so +0 -0
- amsdal/schemas/mixins/check_dependencies_mixin.py +23 -8
- amsdal/schemas/mixins/check_dependencies_mixin.pyi +5 -2
- amsdal/schemas/utils.pyi +2 -2
- 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 +1 -0
- 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/utils/rollback/__init__.pyi +6 -0
- amsdal/utils/tests/enums.py +0 -2
- amsdal/utils/tests/helpers.py +213 -381
- amsdal/utils/tests/migrations.py +157 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/METADATA +13 -8
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/RECORD +131 -124
- {amsdal-0.4.10.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 -44
- amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -44
- amsdal/contrib/auth/models/login_session.pyi +0 -37
- amsdal/contrib/auth/models/permission.pyi +0 -18
- amsdal/contrib/auth/models/user.pyi +0 -46
- amsdal/contrib/frontend_configs/models/frontend_activator_config.pyi +0 -12
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator.pyi +0 -7
- amsdal/contrib/frontend_configs/models/frontend_config_control_action.pyi +0 -32
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator.pyi +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_option.pyi +0 -8
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.pyi +0 -8
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option.pyi +0 -9
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask.pyi +0 -10
- amsdal/contrib/frontend_configs/models/frontend_config_validator.pyi +0 -15
- amsdal/contrib/frontend_configs/models/frontend_control_config.pyi +0 -35
- amsdal/contrib/frontend_configs/models/frontend_model_config.pyi +0 -9
- amsdal/models/__init__.pyi +0 -9
- amsdal/models/core/class_object.pyi +0 -24
- amsdal/models/core/class_object_meta.py +0 -26
- amsdal/models/core/class_object_meta.pyi +0 -15
- amsdal/models/core/class_property.pyi +0 -11
- amsdal/models/core/class_property_meta.py +0 -15
- amsdal/models/core/class_property_meta.pyi +0 -10
- amsdal/models/core/file.pyi +0 -104
- amsdal/models/core/fixture.pyi +0 -14
- amsdal/models/core/option.pyi +0 -8
- amsdal/models/core/validator.pyi +0 -8
- amsdal/models/types/object.pyi +0 -16
- 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/contrib/auth/{models → services}/__init__.pyi +0 -0
- /amsdal/contrib/{frontend_configs/models → auth/transactions}/__init__.pyi +0 -0
- /amsdal/{models/core/__init__.pyi → contrib/auth/utils/__init__.py} +0 -0
- /amsdal/{models/types → contrib/auth/utils}/__init__.pyi +0 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/licenses/LICENSE.txt +0 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/top_level.txt +0 -0
|
@@ -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,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})'
|
|
@@ -47,6 +47,56 @@ class User(Model):
|
|
|
47
47
|
"""
|
|
48
48
|
return self.email
|
|
49
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
|
+
|
|
50
100
|
def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
|
|
51
101
|
"""
|
|
52
102
|
Post-initializes a user object by validating email and password, and hashing the password.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MFA device management services."""
|