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,158 @@
|
|
|
1
|
+
from amsdal.contrib.auth.errors import InvalidMFACodeError as InvalidMFACodeError, MFADeviceNotFoundError as MFADeviceNotFoundError, MFASetupError as MFASetupError, PermissionDeniedError as PermissionDeniedError, UserNotFoundError as UserNotFoundError
|
|
2
|
+
from amsdal.contrib.auth.models.totp_device import TOTPDevice as TOTPDevice
|
|
3
|
+
from amsdal.contrib.auth.models.user import User as User
|
|
4
|
+
from amsdal.contrib.auth.settings import auth_settings as auth_settings
|
|
5
|
+
from amsdal.contrib.auth.utils.mfa import generate_qr_code_url as generate_qr_code_url, generate_totp_secret as generate_totp_secret
|
|
6
|
+
from amsdal_data.transactions.decorators import async_transaction, transaction
|
|
7
|
+
|
|
8
|
+
class TOTPService:
|
|
9
|
+
"""Service for TOTP device two-step enrollment flow."""
|
|
10
|
+
@classmethod
|
|
11
|
+
def _is_admin(cls, user: User) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if user has admin permissions (wildcard or MFADevice-specific).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
user: The user to check.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
bool: True if user has admin permissions, False otherwise.
|
|
20
|
+
"""
|
|
21
|
+
@classmethod
|
|
22
|
+
def _check_permission(cls, current_user: User, target_user_email: str, action: str) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Check if current_user has permission for action on target_user.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
current_user: The authenticated user making the request.
|
|
28
|
+
target_user_email: Email of the user being targeted.
|
|
29
|
+
action: The action being performed.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
PermissionDeniedError: If user lacks permission.
|
|
33
|
+
"""
|
|
34
|
+
@classmethod
|
|
35
|
+
async def _acheck_permission(cls, current_user: User, target_user_email: str, action: str) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Async version of _check_permission.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
current_user: The authenticated user making the request.
|
|
41
|
+
target_user_email: Email of the user being targeted.
|
|
42
|
+
action: The action being performed.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
PermissionDeniedError: If user lacks permission.
|
|
46
|
+
"""
|
|
47
|
+
@classmethod
|
|
48
|
+
@transaction
|
|
49
|
+
def setup_totp_device(cls, current_user: User, target_user_email: str, device_name: str, issuer: str | None = None) -> dict[str, str]:
|
|
50
|
+
"""
|
|
51
|
+
Step 1: Setup TOTP device (generate secret, create unconfirmed device).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
current_user: The authenticated user making the request.
|
|
55
|
+
target_user_email: Email of user to setup device for.
|
|
56
|
+
device_name: User-friendly name for the device.
|
|
57
|
+
issuer: TOTP issuer name (defaults to MFA_TOTP_ISSUER setting).
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
dict with keys:
|
|
61
|
+
- secret: Base32 secret (show to user ONCE)
|
|
62
|
+
- qr_code_url: otpauth:// URL for QR code generation
|
|
63
|
+
- device_id: ID of unconfirmed device
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
UserNotFoundError: If target user doesn't exist.
|
|
67
|
+
PermissionDeniedError: If user lacks permission.
|
|
68
|
+
|
|
69
|
+
Security Note:
|
|
70
|
+
Secret is returned ONLY during setup.
|
|
71
|
+
User must scan QR or manually enter secret.
|
|
72
|
+
Device is created with confirmed=False.
|
|
73
|
+
"""
|
|
74
|
+
@classmethod
|
|
75
|
+
@async_transaction
|
|
76
|
+
async def asetup_totp_device(cls, current_user: User, target_user_email: str, device_name: str, issuer: str | None = None) -> dict[str, str]:
|
|
77
|
+
"""
|
|
78
|
+
Async version of setup_totp_device.
|
|
79
|
+
|
|
80
|
+
Step 1: Setup TOTP device (generate secret, create unconfirmed device).
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
current_user: The authenticated user making the request.
|
|
84
|
+
target_user_email: Email of user to setup device for.
|
|
85
|
+
device_name: User-friendly name for the device.
|
|
86
|
+
issuer: TOTP issuer name (defaults to MFA_TOTP_ISSUER setting).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
dict with keys:
|
|
90
|
+
- secret: Base32 secret (show to user ONCE)
|
|
91
|
+
- qr_code_url: otpauth:// URL for QR code generation
|
|
92
|
+
- device_id: ID of unconfirmed device
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
UserNotFoundError: If target user doesn't exist.
|
|
96
|
+
PermissionDeniedError: If user lacks permission.
|
|
97
|
+
|
|
98
|
+
Security Note:
|
|
99
|
+
Secret is returned ONLY during setup.
|
|
100
|
+
User must scan QR or manually enter secret.
|
|
101
|
+
Device is created with confirmed=False.
|
|
102
|
+
"""
|
|
103
|
+
@classmethod
|
|
104
|
+
@transaction
|
|
105
|
+
def confirm_totp_device(cls, current_user: User, device_id: str, verification_code: str) -> TOTPDevice:
|
|
106
|
+
"""
|
|
107
|
+
Step 2: Confirm TOTP device by verifying code.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
current_user: The authenticated user making the request.
|
|
111
|
+
device_id: ID of unconfirmed device from setup step.
|
|
112
|
+
verification_code: 6-digit code from authenticator app.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
TOTPDevice: The confirmed device.
|
|
116
|
+
|
|
117
|
+
Raises:
|
|
118
|
+
PermissionDeniedError: If user lacks permission.
|
|
119
|
+
MFADeviceNotFoundError: If device doesn't exist.
|
|
120
|
+
InvalidMFACodeError: If verification code is incorrect.
|
|
121
|
+
MFASetupError: If device already confirmed.
|
|
122
|
+
|
|
123
|
+
Flow:
|
|
124
|
+
1. Retrieve unconfirmed device
|
|
125
|
+
2. Check ownership/permissions
|
|
126
|
+
3. Verify code using device.verify_code()
|
|
127
|
+
4. Mark device as confirmed=True
|
|
128
|
+
5. Save and return
|
|
129
|
+
"""
|
|
130
|
+
@classmethod
|
|
131
|
+
@async_transaction
|
|
132
|
+
async def aconfirm_totp_device(cls, current_user: User, device_id: str, verification_code: str) -> TOTPDevice:
|
|
133
|
+
"""
|
|
134
|
+
Async version of confirm_totp_device.
|
|
135
|
+
|
|
136
|
+
Step 2: Confirm TOTP device by verifying code.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
current_user: The authenticated user making the request.
|
|
140
|
+
device_id: ID of unconfirmed device from setup step.
|
|
141
|
+
verification_code: 6-digit code from authenticator app.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
TOTPDevice: The confirmed device.
|
|
145
|
+
|
|
146
|
+
Raises:
|
|
147
|
+
PermissionDeniedError: If user lacks permission.
|
|
148
|
+
MFADeviceNotFoundError: If device doesn't exist.
|
|
149
|
+
InvalidMFACodeError: If verification code is incorrect.
|
|
150
|
+
MFASetupError: If device already confirmed.
|
|
151
|
+
|
|
152
|
+
Flow:
|
|
153
|
+
1. Retrieve unconfirmed device
|
|
154
|
+
2. Check ownership/permissions
|
|
155
|
+
3. Verify code using device.verify_code()
|
|
156
|
+
4. Mark device as confirmed=True
|
|
157
|
+
5. Save and return
|
|
158
|
+
"""
|
amsdal/contrib/auth/settings.py
CHANGED
|
@@ -16,6 +16,10 @@ class Settings(BaseSettings):
|
|
|
16
16
|
AUTH_JWT_KEY (str | None): The key used for JWT authentication.
|
|
17
17
|
AUTH_TOKEN_EXPIRATION (int): The expiration time for authentication tokens in seconds.
|
|
18
18
|
REQUIRE_DEFAULT_AUTHORIZATION (bool): Flag to require default authorization.
|
|
19
|
+
REQUIRE_MFA_BY_DEFAULT (bool): Flag to require MFA for all users by default.
|
|
20
|
+
MFA_TOTP_ISSUER (str): The issuer name displayed in TOTP authenticator apps.
|
|
21
|
+
MFA_BACKUP_CODES_COUNT (int): Number of backup codes to generate per user.
|
|
22
|
+
MFA_EMAIL_CODE_EXPIRATION (int): Email MFA code expiration time in seconds.
|
|
19
23
|
"""
|
|
20
24
|
|
|
21
25
|
model_config = SettingsConfigDict(
|
|
@@ -31,6 +35,10 @@ class Settings(BaseSettings):
|
|
|
31
35
|
AUTH_JWT_KEY: str | None = None
|
|
32
36
|
AUTH_TOKEN_EXPIRATION: int = 86400
|
|
33
37
|
REQUIRE_DEFAULT_AUTHORIZATION: bool = True
|
|
38
|
+
REQUIRE_MFA_BY_DEFAULT: bool = False
|
|
39
|
+
MFA_TOTP_ISSUER: str = 'AMSDAL'
|
|
40
|
+
MFA_BACKUP_CODES_COUNT: int = 10
|
|
41
|
+
MFA_EMAIL_CODE_EXPIRATION: int = 300
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
auth_settings = Settings()
|
amsdal/contrib/auth/settings.pyi
CHANGED
|
@@ -15,6 +15,10 @@ class Settings(BaseSettings):
|
|
|
15
15
|
AUTH_JWT_KEY (str | None): The key used for JWT authentication.
|
|
16
16
|
AUTH_TOKEN_EXPIRATION (int): The expiration time for authentication tokens in seconds.
|
|
17
17
|
REQUIRE_DEFAULT_AUTHORIZATION (bool): Flag to require default authorization.
|
|
18
|
+
REQUIRE_MFA_BY_DEFAULT (bool): Flag to require MFA for all users by default.
|
|
19
|
+
MFA_TOTP_ISSUER (str): The issuer name displayed in TOTP authenticator apps.
|
|
20
|
+
MFA_BACKUP_CODES_COUNT (int): Number of backup codes to generate per user.
|
|
21
|
+
MFA_EMAIL_CODE_EXPIRATION (int): Email MFA code expiration time in seconds.
|
|
18
22
|
"""
|
|
19
23
|
model_config: Incomplete
|
|
20
24
|
ADMIN_USER_EMAIL: str | None
|
|
@@ -22,5 +26,9 @@ class Settings(BaseSettings):
|
|
|
22
26
|
AUTH_JWT_KEY: str | None
|
|
23
27
|
AUTH_TOKEN_EXPIRATION: int
|
|
24
28
|
REQUIRE_DEFAULT_AUTHORIZATION: bool
|
|
29
|
+
REQUIRE_MFA_BY_DEFAULT: bool
|
|
30
|
+
MFA_TOTP_ISSUER: str
|
|
31
|
+
MFA_BACKUP_CODES_COUNT: int
|
|
32
|
+
MFA_EMAIL_CODE_EXPIRATION: int
|
|
25
33
|
|
|
26
34
|
auth_settings: Incomplete
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MFA transaction wrappers for API endpoints."""
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""MFA device transaction wrappers for API endpoints."""
|
|
2
|
+
|
|
3
|
+
from amsdal_data.transactions.decorators import async_transaction
|
|
4
|
+
from amsdal_data.transactions.decorators import transaction
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from pydantic import ConfigDict
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from amsdal.context.manager import AmsdalContextManager
|
|
10
|
+
from amsdal.contrib.auth.decorators import require_auth
|
|
11
|
+
from amsdal.contrib.auth.models.email_mfa_device import EmailMFADevice
|
|
12
|
+
from amsdal.contrib.auth.models.user import User
|
|
13
|
+
from amsdal.contrib.auth.services.mfa_device_service import MFADeviceService
|
|
14
|
+
from amsdal.contrib.auth.utils.mfa import DeviceType
|
|
15
|
+
|
|
16
|
+
# ============================================================================
|
|
17
|
+
# REQUEST/RESPONSE MODELS
|
|
18
|
+
# ============================================================================
|
|
19
|
+
|
|
20
|
+
TAGS = ['Auth', 'MFA']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AddEmailDeviceRequest(BaseModel):
|
|
24
|
+
"""Request model for adding email MFA device."""
|
|
25
|
+
|
|
26
|
+
target_user_email: str = Field(..., description='Email of user to add device for')
|
|
27
|
+
device_name: str = Field(..., description='User-friendly name for the device')
|
|
28
|
+
email: str | None = Field(None, description='Email for MFA codes (defaults to target_user_email)')
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EmailDeviceResponse(BaseModel):
|
|
32
|
+
"""Response model for email MFA device."""
|
|
33
|
+
|
|
34
|
+
device_id: str = Field(..., description='Device ID')
|
|
35
|
+
user_email: str = Field(..., description='User email')
|
|
36
|
+
name: str = Field(..., description='Device name')
|
|
37
|
+
email: str = Field(..., description='Email where MFA codes are sent')
|
|
38
|
+
confirmed: bool = Field(..., description='Whether device is confirmed')
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_device(cls, device: EmailMFADevice) -> 'EmailDeviceResponse':
|
|
42
|
+
"""Create response from EmailMFADevice model."""
|
|
43
|
+
return cls(
|
|
44
|
+
device_id=device._object_id,
|
|
45
|
+
user_email=device.user_email,
|
|
46
|
+
name=device.name,
|
|
47
|
+
email=device.email,
|
|
48
|
+
confirmed=device.confirmed,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AddBackupCodesRequest(BaseModel):
|
|
53
|
+
"""Request model for adding backup codes."""
|
|
54
|
+
|
|
55
|
+
target_user_email: str = Field(..., description='Email of user to add codes for')
|
|
56
|
+
device_name: str = Field('Backup Codes', description='Name for the backup code set')
|
|
57
|
+
code_count: int | None = Field(None, description='Number of codes to generate (optional)', ge=1, le=20)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AddBackupCodesResponse(BaseModel):
|
|
61
|
+
"""Response model for adding backup codes."""
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(
|
|
64
|
+
json_schema_extra={
|
|
65
|
+
'example': {
|
|
66
|
+
'device_count': 10,
|
|
67
|
+
'codes': ['ABC123DEF456', 'GHI789JKL012', '...'],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
device_count: int = Field(..., description='Number of backup codes generated')
|
|
73
|
+
codes: list[str] = Field(..., description='Plaintext backup codes (DISPLAY ONCE)')
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ListDevicesRequest(BaseModel):
|
|
77
|
+
"""Request model for listing MFA devices."""
|
|
78
|
+
|
|
79
|
+
target_user_email: str = Field(..., description='Email of user to list devices for')
|
|
80
|
+
include_unconfirmed: bool = Field(False, description='Whether to include unconfirmed devices')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DeviceInfo(BaseModel):
|
|
84
|
+
"""Device information for list response."""
|
|
85
|
+
|
|
86
|
+
device_id: str = Field(..., description='Device ID')
|
|
87
|
+
name: str = Field(..., description='Device name')
|
|
88
|
+
confirmed: bool = Field(..., description='Whether device is confirmed')
|
|
89
|
+
device_type: str = Field(..., description='Type of device')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ListDevicesResponse(BaseModel):
|
|
93
|
+
"""Response model for listing MFA devices."""
|
|
94
|
+
|
|
95
|
+
totp_devices: list[DeviceInfo] = Field(default_factory=list, description='TOTP authenticator devices')
|
|
96
|
+
email_devices: list[DeviceInfo] = Field(default_factory=list, description='Email MFA devices')
|
|
97
|
+
sms_devices: list[DeviceInfo] = Field(default_factory=list, description='SMS MFA devices')
|
|
98
|
+
backup_codes: list[DeviceInfo] = Field(default_factory=list, description='Backup code devices')
|
|
99
|
+
total_count: int = Field(..., description='Total number of devices')
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def from_device_dict(cls, devices_by_type: dict) -> 'ListDevicesResponse': # type: ignore[type-arg]
|
|
103
|
+
"""Create response from service layer result."""
|
|
104
|
+
totp_devices = [
|
|
105
|
+
DeviceInfo(
|
|
106
|
+
device_id=device._object_id,
|
|
107
|
+
name=device.name,
|
|
108
|
+
confirmed=device.confirmed,
|
|
109
|
+
device_type='totp',
|
|
110
|
+
)
|
|
111
|
+
for device in devices_by_type.get(DeviceType.TOTP, [])
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
email_devices = [
|
|
115
|
+
DeviceInfo(
|
|
116
|
+
device_id=device._object_id,
|
|
117
|
+
name=device.name,
|
|
118
|
+
confirmed=device.confirmed,
|
|
119
|
+
device_type='email',
|
|
120
|
+
)
|
|
121
|
+
for device in devices_by_type.get(DeviceType.EMAIL, [])
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
sms_devices = [
|
|
125
|
+
DeviceInfo(
|
|
126
|
+
device_id=device._object_id,
|
|
127
|
+
name=device.name,
|
|
128
|
+
confirmed=device.confirmed,
|
|
129
|
+
device_type='sms',
|
|
130
|
+
)
|
|
131
|
+
for device in devices_by_type.get(DeviceType.SMS, [])
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
backup_codes = [
|
|
135
|
+
DeviceInfo(
|
|
136
|
+
device_id=device._object_id,
|
|
137
|
+
name=device.name,
|
|
138
|
+
confirmed=device.confirmed,
|
|
139
|
+
device_type='backup_code',
|
|
140
|
+
)
|
|
141
|
+
for device in devices_by_type.get(DeviceType.BACKUP_CODE, [])
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
total = len(totp_devices) + len(email_devices) + len(sms_devices) + len(backup_codes)
|
|
145
|
+
|
|
146
|
+
return cls(
|
|
147
|
+
totp_devices=totp_devices,
|
|
148
|
+
email_devices=email_devices,
|
|
149
|
+
sms_devices=sms_devices,
|
|
150
|
+
backup_codes=backup_codes,
|
|
151
|
+
total_count=total,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class RemoveDeviceRequest(BaseModel):
|
|
156
|
+
"""Request model for removing MFA device."""
|
|
157
|
+
|
|
158
|
+
device_id: str = Field(..., description='ID of device to remove')
|
|
159
|
+
hard_delete: bool = Field(False, description='If True, permanently delete; if False, soft delete')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class RemoveDeviceResponse(BaseModel):
|
|
163
|
+
"""Response model for removing MFA device."""
|
|
164
|
+
|
|
165
|
+
device_id: str = Field(..., description='ID of removed device')
|
|
166
|
+
deleted: bool = Field(..., description='Whether device was permanently deleted')
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ============================================================================
|
|
170
|
+
# TRANSACTION FUNCTIONS
|
|
171
|
+
# ============================================================================
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_current_user() -> User:
|
|
175
|
+
"""Helper to get current authenticated user from context."""
|
|
176
|
+
return AmsdalContextManager().get_context().get('request').user # type: ignore[union-attr]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@require_auth
|
|
180
|
+
@transaction(tags=TAGS) # type: ignore[call-arg]
|
|
181
|
+
def add_email_device_transaction(
|
|
182
|
+
request: AddEmailDeviceRequest,
|
|
183
|
+
) -> EmailDeviceResponse:
|
|
184
|
+
"""
|
|
185
|
+
Add email MFA device for a user.
|
|
186
|
+
|
|
187
|
+
Email devices are automatically confirmed and can be used immediately.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
current_user: The authenticated user making the request.
|
|
191
|
+
request: Email device creation request.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
EmailDeviceResponse: Created device details.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
UserNotFoundError: If target user doesn't exist.
|
|
198
|
+
PermissionDeniedError: If user lacks permission.
|
|
199
|
+
"""
|
|
200
|
+
if isinstance(request, dict):
|
|
201
|
+
request = AddEmailDeviceRequest.model_validate(request)
|
|
202
|
+
|
|
203
|
+
device = MFADeviceService.add_email_device( # type: ignore[call-arg]
|
|
204
|
+
current_user=get_current_user(),
|
|
205
|
+
target_user_email=request.target_user_email,
|
|
206
|
+
device_name=request.device_name,
|
|
207
|
+
email=request.email,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return EmailDeviceResponse.from_device(device)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@require_auth
|
|
214
|
+
@async_transaction(tags=TAGS) # type: ignore[call-arg]
|
|
215
|
+
async def aadd_email_device_transaction(
|
|
216
|
+
request: AddEmailDeviceRequest,
|
|
217
|
+
) -> EmailDeviceResponse:
|
|
218
|
+
"""
|
|
219
|
+
Async version of add_email_device_transaction.
|
|
220
|
+
|
|
221
|
+
Add email MFA device for a user.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
current_user: The authenticated user making the request.
|
|
225
|
+
request: Email device creation request.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
EmailDeviceResponse: Created device details.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
UserNotFoundError: If target user doesn't exist.
|
|
232
|
+
PermissionDeniedError: If user lacks permission.
|
|
233
|
+
"""
|
|
234
|
+
if isinstance(request, dict):
|
|
235
|
+
request = AddEmailDeviceRequest.model_validate(request)
|
|
236
|
+
|
|
237
|
+
device = await MFADeviceService.aadd_email_device( # type: ignore[call-arg]
|
|
238
|
+
current_user=get_current_user(),
|
|
239
|
+
target_user_email=request.target_user_email,
|
|
240
|
+
device_name=request.device_name,
|
|
241
|
+
email=request.email,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return EmailDeviceResponse.from_device(device)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@require_auth
|
|
248
|
+
@transaction(tags=TAGS) # type: ignore[call-arg]
|
|
249
|
+
def add_backup_codes_transaction(
|
|
250
|
+
request: AddBackupCodesRequest,
|
|
251
|
+
) -> AddBackupCodesResponse:
|
|
252
|
+
"""
|
|
253
|
+
Add backup codes for a user.
|
|
254
|
+
|
|
255
|
+
Backup codes are one-time use codes that can be used if primary
|
|
256
|
+
MFA methods are unavailable.
|
|
257
|
+
|
|
258
|
+
Security Note:
|
|
259
|
+
Plaintext codes are returned ONLY during creation.
|
|
260
|
+
Caller must display/send these to user immediately.
|
|
261
|
+
Codes cannot be retrieved later.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
current_user: The authenticated user making the request.
|
|
265
|
+
request: Backup codes creation request.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
AddBackupCodesResponse: Contains plaintext backup codes.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
UserNotFoundError: If target user doesn't exist.
|
|
272
|
+
PermissionDeniedError: If user lacks permission.
|
|
273
|
+
"""
|
|
274
|
+
if isinstance(request, dict):
|
|
275
|
+
request = AddBackupCodesRequest.model_validate(request)
|
|
276
|
+
|
|
277
|
+
devices, plaintext_codes = MFADeviceService.add_backup_codes( # type: ignore[call-arg]
|
|
278
|
+
current_user=get_current_user(),
|
|
279
|
+
target_user_email=request.target_user_email,
|
|
280
|
+
device_name=request.device_name,
|
|
281
|
+
code_count=request.code_count,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return AddBackupCodesResponse(
|
|
285
|
+
device_count=len(devices),
|
|
286
|
+
codes=plaintext_codes,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@require_auth
|
|
291
|
+
@async_transaction(tags=TAGS) # type: ignore[call-arg]
|
|
292
|
+
async def aadd_backup_codes_transaction(
|
|
293
|
+
request: AddBackupCodesRequest,
|
|
294
|
+
) -> AddBackupCodesResponse:
|
|
295
|
+
"""
|
|
296
|
+
Async version of add_backup_codes_transaction.
|
|
297
|
+
|
|
298
|
+
Add backup codes for a user.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
current_user: The authenticated user making the request.
|
|
302
|
+
request: Backup codes creation request.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
AddBackupCodesResponse: Contains plaintext backup codes.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
UserNotFoundError: If target user doesn't exist.
|
|
309
|
+
PermissionDeniedError: If user lacks permission.
|
|
310
|
+
"""
|
|
311
|
+
if isinstance(request, dict):
|
|
312
|
+
request = AddBackupCodesRequest.model_validate(request)
|
|
313
|
+
|
|
314
|
+
devices, plaintext_codes = await MFADeviceService.aadd_backup_codes( # type: ignore[call-arg]
|
|
315
|
+
current_user=get_current_user(),
|
|
316
|
+
target_user_email=request.target_user_email,
|
|
317
|
+
device_name=request.device_name,
|
|
318
|
+
code_count=request.code_count,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return AddBackupCodesResponse(
|
|
322
|
+
device_count=len(devices),
|
|
323
|
+
codes=plaintext_codes,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@require_auth
|
|
328
|
+
@transaction(tags=TAGS) # type: ignore[call-arg]
|
|
329
|
+
def list_devices_transaction(
|
|
330
|
+
request: ListDevicesRequest,
|
|
331
|
+
) -> ListDevicesResponse:
|
|
332
|
+
"""
|
|
333
|
+
List all MFA devices for a user.
|
|
334
|
+
|
|
335
|
+
Returns devices grouped by type (TOTP, Email, SMS, Backup Codes).
|
|
336
|
+
|
|
337
|
+
Note: Read-only operation, no @transaction decorator needed.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
current_user: The authenticated user making the request.
|
|
341
|
+
request: List devices request.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
ListDevicesResponse: Devices grouped by type.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
PermissionDeniedError: If user lacks permission.
|
|
348
|
+
"""
|
|
349
|
+
if isinstance(request, dict):
|
|
350
|
+
request = ListDevicesRequest.model_validate(request)
|
|
351
|
+
|
|
352
|
+
devices_by_type = MFADeviceService.list_devices(
|
|
353
|
+
current_user=get_current_user(),
|
|
354
|
+
target_user_email=request.target_user_email,
|
|
355
|
+
include_unconfirmed=request.include_unconfirmed,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
return ListDevicesResponse.from_device_dict(devices_by_type)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@require_auth
|
|
362
|
+
@async_transaction(tags=TAGS) # type: ignore[call-arg]
|
|
363
|
+
async def alist_devices_transaction(
|
|
364
|
+
request: ListDevicesRequest,
|
|
365
|
+
) -> ListDevicesResponse:
|
|
366
|
+
"""
|
|
367
|
+
Async version of list_devices_transaction.
|
|
368
|
+
|
|
369
|
+
List all MFA devices for a user.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
current_user: The authenticated user making the request.
|
|
373
|
+
request: List devices request.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
ListDevicesResponse: Devices grouped by type.
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
PermissionDeniedError: If user lacks permission.
|
|
380
|
+
"""
|
|
381
|
+
if isinstance(request, dict):
|
|
382
|
+
request = ListDevicesRequest.model_validate(request)
|
|
383
|
+
|
|
384
|
+
devices_by_type = await MFADeviceService.alist_devices(
|
|
385
|
+
current_user=get_current_user(),
|
|
386
|
+
target_user_email=request.target_user_email,
|
|
387
|
+
include_unconfirmed=request.include_unconfirmed,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return ListDevicesResponse.from_device_dict(devices_by_type)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
@require_auth
|
|
394
|
+
@transaction(tags=TAGS) # type: ignore[call-arg]
|
|
395
|
+
def remove_device_transaction(
|
|
396
|
+
request: RemoveDeviceRequest,
|
|
397
|
+
) -> RemoveDeviceResponse:
|
|
398
|
+
"""
|
|
399
|
+
Remove (deactivate or delete) an MFA device.
|
|
400
|
+
|
|
401
|
+
By default performs soft delete (marks inactive) to preserve audit trail.
|
|
402
|
+
Use hard_delete=True to permanently remove the device.
|
|
403
|
+
|
|
404
|
+
Args:
|
|
405
|
+
current_user: The authenticated user making the request.
|
|
406
|
+
request: Remove device request.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
RemoveDeviceResponse: Removal confirmation.
|
|
410
|
+
|
|
411
|
+
Raises:
|
|
412
|
+
PermissionDeniedError: If user lacks permission.
|
|
413
|
+
MFADeviceNotFoundError: If device doesn't exist.
|
|
414
|
+
"""
|
|
415
|
+
if isinstance(request, dict):
|
|
416
|
+
request = RemoveDeviceRequest.model_validate(request)
|
|
417
|
+
|
|
418
|
+
MFADeviceService.remove_device( # type: ignore[call-arg]
|
|
419
|
+
current_user=get_current_user(),
|
|
420
|
+
device_id=request.device_id,
|
|
421
|
+
hard_delete=request.hard_delete,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
return RemoveDeviceResponse(
|
|
425
|
+
device_id=request.device_id,
|
|
426
|
+
deleted=request.hard_delete,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@require_auth
|
|
431
|
+
@async_transaction(tags=TAGS) # type: ignore[call-arg]
|
|
432
|
+
async def aremove_device_transaction(
|
|
433
|
+
request: RemoveDeviceRequest,
|
|
434
|
+
) -> RemoveDeviceResponse:
|
|
435
|
+
"""
|
|
436
|
+
Async version of remove_device_transaction.
|
|
437
|
+
|
|
438
|
+
Remove (deactivate or delete) an MFA device.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
current_user: The authenticated user making the request.
|
|
442
|
+
request: Remove device request.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
RemoveDeviceResponse: Removal confirmation.
|
|
446
|
+
|
|
447
|
+
Raises:
|
|
448
|
+
PermissionDeniedError: If user lacks permission.
|
|
449
|
+
MFADeviceNotFoundError: If device doesn't exist.
|
|
450
|
+
"""
|
|
451
|
+
if isinstance(request, dict):
|
|
452
|
+
request = RemoveDeviceRequest.model_validate(request)
|
|
453
|
+
|
|
454
|
+
await MFADeviceService.aremove_device( # type: ignore[call-arg]
|
|
455
|
+
current_user=get_current_user(),
|
|
456
|
+
device_id=request.device_id,
|
|
457
|
+
hard_delete=request.hard_delete,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
return RemoveDeviceResponse(
|
|
461
|
+
device_id=request.device_id,
|
|
462
|
+
deleted=request.hard_delete,
|
|
463
|
+
)
|