python-platform 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_platform/__init__.py +445 -0
- python_platform/application/__init__.py +6 -0
- python_platform/application/build_spec.py +37 -0
- python_platform/application/builder.py +260 -0
- python_platform/application/composition.py +219 -0
- python_platform/application/runtime.py +546 -0
- python_platform/application/runtime_hooks.py +327 -0
- python_platform/application/state.py +216 -0
- python_platform/application_services/__init__.py +54 -0
- python_platform/application_services/catalog.py +500 -0
- python_platform/application_services/contracts.py +188 -0
- python_platform/application_services/dispatcher.py +423 -0
- python_platform/application_services/errors.py +89 -0
- python_platform/application_services/execution.py +199 -0
- python_platform/application_services/interceptors.py +44 -0
- python_platform/application_services/invocation.py +427 -0
- python_platform/application_services/policies.py +46 -0
- python_platform/application_services/seeding.py +72 -0
- python_platform/application_services/signature.py +49 -0
- python_platform/application_services/validation.py +84 -0
- python_platform/auditing/__init__.py +12 -0
- python_platform/auditing/contracts.py +39 -0
- python_platform/auditing/control.py +48 -0
- python_platform/auditing/sqlalchemy/__init__.py +6 -0
- python_platform/auditing/sqlalchemy/migrations/0001_platform_auditing.py +45 -0
- python_platform/auditing/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/auditing/sqlalchemy/models.py +31 -0
- python_platform/auditing/sqlalchemy/module.py +32 -0
- python_platform/auditing/sqlalchemy/store.py +63 -0
- python_platform/authorization/__init__.py +38 -0
- python_platform/authorization/catalog.py +63 -0
- python_platform/authorization/contracts.py +241 -0
- python_platform/authorization/definition_discovery.py +81 -0
- python_platform/authorization/definitions.py +43 -0
- python_platform/authorization/errors.py +40 -0
- python_platform/background_execution/__init__.py +15 -0
- python_platform/background_execution/application.py +41 -0
- python_platform/background_execution/child.py +123 -0
- python_platform/background_execution/contracts.py +39 -0
- python_platform/background_execution/lifecycle.py +22 -0
- python_platform/background_execution/local.py +127 -0
- python_platform/background_execution/locks.py +31 -0
- python_platform/background_execution/management.py +17 -0
- python_platform/background_execution/module.py +10 -0
- python_platform/background_execution/permissions.py +14 -0
- python_platform/background_execution/processes.py +331 -0
- python_platform/background_jobs/__init__.py +31 -0
- python_platform/background_jobs/catalog.py +287 -0
- python_platform/background_jobs/contracts.py +202 -0
- python_platform/background_jobs/errors.py +28 -0
- python_platform/background_jobs/execution.py +201 -0
- python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
- python_platform/background_jobs/pgqueuer/__init__.py +6 -0
- python_platform/background_jobs/pgqueuer/enqueue.py +82 -0
- python_platform/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +63 -0
- python_platform/background_jobs/pgqueuer/migrations/__init__.py +1 -0
- python_platform/background_jobs/pgqueuer/module.py +102 -0
- python_platform/background_jobs/pgqueuer/options.py +70 -0
- python_platform/background_jobs/pgqueuer/runtime.py +479 -0
- python_platform/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
- python_platform/background_jobs/pgqueuer/supervision.py +201 -0
- python_platform/background_workers/__init__.py +22 -0
- python_platform/background_workers/catalog.py +96 -0
- python_platform/background_workers/contracts.py +124 -0
- python_platform/background_workers/errors.py +20 -0
- python_platform/background_workers/execution.py +133 -0
- python_platform/background_workers/runtime.py +203 -0
- python_platform/caching/__init__.py +11 -0
- python_platform/caching/catalog.py +73 -0
- python_platform/caching/contracts.py +73 -0
- python_platform/caching/errors.py +20 -0
- python_platform/cli/__init__.py +87 -0
- python_platform/cli/errors.py +5 -0
- python_platform/cli/project.py +69 -0
- python_platform/cli/runtime.py +68 -0
- python_platform/configuration/__init__.py +21 -0
- python_platform/configuration/composition.py +65 -0
- python_platform/configuration/contracts.py +68 -0
- python_platform/configuration/dotenv_source.py +51 -0
- python_platform/configuration/environment_source.py +38 -0
- python_platform/configuration/immutability.py +124 -0
- python_platform/configuration/input_shape.py +58 -0
- python_platform/configuration/merge.py +103 -0
- python_platform/configuration/root.py +215 -0
- python_platform/configuration/sources.py +519 -0
- python_platform/configuration/values.py +427 -0
- python_platform/configuration/yaml_source.py +51 -0
- python_platform/developer_kit/__init__.py +1 -0
- python_platform/developer_kit/generation.py +182 -0
- python_platform/developer_kit/project_metadata.py +45 -0
- python_platform/developer_kit/source.py +53 -0
- python_platform/developer_kit/templates/module/cookiecutter.json +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +46 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +29 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +8 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +81 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +86 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +36 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +57 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +12 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +74 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +58 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +37 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models.py.jinja +26 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +32 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repository.py.jinja +49 -0
- python_platform/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
- python_platform/developer_kit/templates/project/cookiecutter.json +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.env.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +86 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/app.yaml.jinja +16 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +13 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +52 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +38 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
- python_platform/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +101 -0
- python_platform/developer_kit/wiring.py +85 -0
- python_platform/diagnostics/__init__.py +23 -0
- python_platform/diagnostics/journal.py +174 -0
- python_platform/diagnostics/model.py +83 -0
- python_platform/diagnostics/source.py +25 -0
- python_platform/distributed_lock/__init__.py +6 -0
- python_platform/distributed_lock/contracts.py +20 -0
- python_platform/distributed_lock/options.py +19 -0
- python_platform/domain/__init__.py +13 -0
- python_platform/domain/aggregates.py +64 -0
- python_platform/domain/entities.py +33 -0
- python_platform/domain/errors.py +12 -0
- python_platform/domain/value_objects.py +31 -0
- python_platform/errors/__init__.py +75 -0
- python_platform/errors/base.py +13 -0
- python_platform/errors/business.py +53 -0
- python_platform/errors/configuration.py +119 -0
- python_platform/errors/diagnostics.py +12 -0
- python_platform/errors/lifecycle.py +164 -0
- python_platform/errors/modularity.py +100 -0
- python_platform/errors/services.py +100 -0
- python_platform/events/__init__.py +25 -0
- python_platform/events/aggregate.py +39 -0
- python_platform/events/catalog.py +136 -0
- python_platform/events/contracts.py +39 -0
- python_platform/events/contribution.py +128 -0
- python_platform/events/discovery.py +105 -0
- python_platform/events/errors.py +48 -0
- python_platform/events/runtime.py +192 -0
- python_platform/fastapi/__init__.py +49 -0
- python_platform/fastapi/action.py +175 -0
- python_platform/fastapi/adapter.py +381 -0
- python_platform/fastapi/application_services.py +647 -0
- python_platform/fastapi/background.py +42 -0
- python_platform/fastapi/contracts.py +278 -0
- python_platform/fastapi/errors.py +32 -0
- python_platform/fastapi/filters.py +111 -0
- python_platform/fastapi/health.py +44 -0
- python_platform/fastapi/http.py +55 -0
- python_platform/fastapi/http_router.py +212 -0
- python_platform/fastapi/manual_action.py +105 -0
- python_platform/fastapi/middleware.py +93 -0
- python_platform/fastapi/parameters.py +97 -0
- python_platform/fastapi/realtime/__init__.py +13 -0
- python_platform/fastapi/realtime/authentication.py +181 -0
- python_platform/fastapi/realtime/connection.py +166 -0
- python_platform/fastapi/realtime/module.py +41 -0
- python_platform/fastapi/realtime/options.py +50 -0
- python_platform/fastapi/realtime/runtime.py +423 -0
- python_platform/fastapi/request_context.py +244 -0
- python_platform/fastapi/route_integrity.py +88 -0
- python_platform/fastapi/routing.py +410 -0
- python_platform/fastapi/server.py +181 -0
- python_platform/fastapi/settings.py +40 -0
- python_platform/fastapi/tracing.py +62 -0
- python_platform/fastapi/transfer.py +128 -0
- python_platform/fastapi/upload_limits.py +63 -0
- python_platform/fastapi/uploads.py +94 -0
- python_platform/hosted_services/__init__.py +29 -0
- python_platform/hosted_services/bridge.py +420 -0
- python_platform/hosted_services/catalog.py +259 -0
- python_platform/hosted_services/contracts.py +55 -0
- python_platform/hosted_services/errors.py +53 -0
- python_platform/hosted_services/options.py +17 -0
- python_platform/hosted_services/runtime.py +267 -0
- python_platform/hosted_services/state.py +40 -0
- python_platform/hosting/__init__.py +4 -0
- python_platform/hosting/instance.py +40 -0
- python_platform/identity/__init__.py +73 -0
- python_platform/identity/application.py +287 -0
- python_platform/identity/contracts.py +283 -0
- python_platform/identity/errors.py +28 -0
- python_platform/identity/http_api.py +90 -0
- python_platform/identity/module.py +46 -0
- python_platform/identity/passwords.py +34 -0
- python_platform/identity/permissions.py +14 -0
- python_platform/identity/services.py +108 -0
- python_platform/identity/sqlalchemy/__init__.py +5 -0
- python_platform/identity/sqlalchemy/migrations/0001_platform_identity.py +151 -0
- python_platform/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
- python_platform/identity/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/identity/sqlalchemy/models.py +85 -0
- python_platform/identity/sqlalchemy/module.py +62 -0
- python_platform/identity/sqlalchemy/stores.py +526 -0
- python_platform/identity/tokens.py +106 -0
- python_platform/invocation/__init__.py +3 -0
- python_platform/invocation/callables.py +171 -0
- python_platform/invocation/contracts.py +33 -0
- python_platform/invocation/entries.py +54 -0
- python_platform/invocation/function_runtime.py +48 -0
- python_platform/invocation/interception.py +184 -0
- python_platform/lifecycle/__init__.py +25 -0
- python_platform/lifecycle/composition.py +35 -0
- python_platform/lifecycle/context.py +31 -0
- python_platform/lifecycle/runtime.py +43 -0
- python_platform/lifecycle/state.py +20 -0
- python_platform/modularity/__init__.py +15 -0
- python_platform/modularity/contracts.py +90 -0
- python_platform/modularity/discovery.py +143 -0
- python_platform/modularity/graph.py +334 -0
- python_platform/modularity/registry.py +100 -0
- python_platform/modularity/selection.py +16 -0
- python_platform/notifications/__init__.py +19 -0
- python_platform/notifications/catalog.py +46 -0
- python_platform/notifications/contracts.py +107 -0
- python_platform/observability/__init__.py +5 -0
- python_platform/observability/context.py +43 -0
- python_platform/observability/export.py +73 -0
- python_platform/observability/formatting.py +89 -0
- python_platform/observability/logging.py +118 -0
- python_platform/observability/options.py +45 -0
- python_platform/observability/tracing.py +80 -0
- python_platform/options/__init__.py +19 -0
- python_platform/options/aliases.py +331 -0
- python_platform/options/contribution.py +62 -0
- python_platform/options/immutability.py +48 -0
- python_platform/options/input_keys.py +214 -0
- python_platform/options/issues.py +335 -0
- python_platform/options/models.py +114 -0
- python_platform/options/registry.py +280 -0
- python_platform/options/schema.py +488 -0
- python_platform/options/validation.py +120 -0
- python_platform/py.typed +0 -0
- python_platform/realtime/__init__.py +12 -0
- python_platform/realtime/contracts.py +28 -0
- python_platform/realtime/diagnostics.py +37 -0
- python_platform/realtime/messages.py +113 -0
- python_platform/redis/__init__.py +6 -0
- python_platform/redis/distributed_lock.py +212 -0
- python_platform/redis/lease_lock.py +36 -0
- python_platform/redis/module.py +50 -0
- python_platform/redis/notification_runtime.py +182 -0
- python_platform/redis/notifications.py +25 -0
- python_platform/redis/options.py +35 -0
- python_platform/redis/runtime.py +116 -0
- python_platform/services/__init__.py +39 -0
- python_platform/services/application_bindings.py +221 -0
- python_platform/services/arbitration.py +381 -0
- python_platform/services/binding.py +102 -0
- python_platform/services/contribution.py +438 -0
- python_platform/services/convention.py +292 -0
- python_platform/services/convention_contracts.py +94 -0
- python_platform/services/exposure.py +23 -0
- python_platform/services/framework_provider.py +178 -0
- python_platform/services/native_graph.py +172 -0
- python_platform/services/provider.py +204 -0
- python_platform/services/registration.py +101 -0
- python_platform/services/runtime.py +298 -0
- python_platform/settings/__init__.py +30 -0
- python_platform/settings/application.py +95 -0
- python_platform/settings/binding.py +33 -0
- python_platform/settings/catalog.py +121 -0
- python_platform/settings/changes.py +14 -0
- python_platform/settings/contracts.py +115 -0
- python_platform/settings/definition_discovery.py +71 -0
- python_platform/settings/definitions.py +41 -0
- python_platform/settings/errors.py +22 -0
- python_platform/settings/handlers.py +43 -0
- python_platform/settings/management.py +57 -0
- python_platform/settings/manager.py +89 -0
- python_platform/settings/module.py +7 -0
- python_platform/settings/notifications.py +39 -0
- python_platform/settings/permissions.py +14 -0
- python_platform/settings/provider.py +74 -0
- python_platform/settings/refresh.py +140 -0
- python_platform/settings/refresh_module.py +45 -0
- python_platform/settings/sqlalchemy/__init__.py +5 -0
- python_platform/settings/sqlalchemy/migrations/0001_platform_settings.py +31 -0
- python_platform/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
- python_platform/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
- python_platform/settings/sqlalchemy/migrations/__init__.py +1 -0
- python_platform/settings/sqlalchemy/models.py +36 -0
- python_platform/settings/sqlalchemy/module.py +36 -0
- python_platform/settings/sqlalchemy/store.py +86 -0
- python_platform/settings/store.py +16 -0
- python_platform/settings/values.py +39 -0
- python_platform/sqlalchemy/__init__.py +51 -0
- python_platform/sqlalchemy/alembic_runtime/__init__.py +1 -0
- python_platform/sqlalchemy/alembic_runtime/env.py +27 -0
- python_platform/sqlalchemy/alembic_runtime/script.py.mako +14 -0
- python_platform/sqlalchemy/auditing.py +48 -0
- python_platform/sqlalchemy/errors.py +85 -0
- python_platform/sqlalchemy/metadata.py +538 -0
- python_platform/sqlalchemy/migration.py +346 -0
- python_platform/sqlalchemy/module.py +48 -0
- python_platform/sqlalchemy/module_migration.py +148 -0
- python_platform/sqlalchemy/options.py +66 -0
- python_platform/sqlalchemy/repository.py +33 -0
- python_platform/sqlalchemy/runtime.py +85 -0
- python_platform/sqlalchemy/session_provider.py +45 -0
- python_platform/sqlalchemy/unit_of_work.py +97 -0
- python_platform/testing/__init__.py +5 -0
- python_platform/testing/runtime.py +112 -0
- python_platform/unit_of_work/__init__.py +14 -0
- python_platform/unit_of_work/contracts.py +223 -0
- python_platform/unit_of_work/errors.py +27 -0
- python_platform/unit_of_work/manager.py +210 -0
- python_platform/unit_of_work/options.py +92 -0
- python_platform-0.1.1.dist-info/METADATA +122 -0
- python_platform-0.1.1.dist-info/RECORD +344 -0
- python_platform-0.1.1.dist-info/WHEEL +4 -0
- python_platform-0.1.1.dist-info/entry_points.txt +9 -0
- python_platform-0.1.1.dist-info/licenses/LICENSE +7 -0
- python_platform-0.1.1.dist-info/licenses/src/python_platform/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Identity Application Services:认证、refresh family 与管理员业务编排。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime, timedelta
|
|
6
|
+
from uuid import UUID, uuid4
|
|
7
|
+
|
|
8
|
+
from ..application_services import ApplicationService, DataSeedContributor, application_service
|
|
9
|
+
from ..authorization import (
|
|
10
|
+
CurrentUser,
|
|
11
|
+
PermissionCatalog,
|
|
12
|
+
PermissionName,
|
|
13
|
+
allow_anonymous,
|
|
14
|
+
authorize,
|
|
15
|
+
)
|
|
16
|
+
from ..options import Options
|
|
17
|
+
from .contracts import (
|
|
18
|
+
IDENTITY_ROLES_MANAGE,
|
|
19
|
+
IDENTITY_USERS_MANAGE,
|
|
20
|
+
AuthenticationApplicationService,
|
|
21
|
+
CreateRoleCommand,
|
|
22
|
+
CreateUserCommand,
|
|
23
|
+
CurrentUserView,
|
|
24
|
+
IdentityLoginStore,
|
|
25
|
+
IdentityManagementApplicationService,
|
|
26
|
+
IdentityOptions,
|
|
27
|
+
IdentityRepository,
|
|
28
|
+
IdentityRole,
|
|
29
|
+
IdentityUser,
|
|
30
|
+
LoginCommand,
|
|
31
|
+
RefreshRotationStatus,
|
|
32
|
+
RefreshSession,
|
|
33
|
+
RefreshSessionStore,
|
|
34
|
+
RefreshTokenCommand,
|
|
35
|
+
RevokeTokenCommand,
|
|
36
|
+
RoleView,
|
|
37
|
+
SetRolePermissionsCommand,
|
|
38
|
+
SetUserRolesCommand,
|
|
39
|
+
TokenPair,
|
|
40
|
+
UserView,
|
|
41
|
+
_normalize_name,
|
|
42
|
+
)
|
|
43
|
+
from .errors import (
|
|
44
|
+
IdentityConflictError,
|
|
45
|
+
IdentityResourceNotFoundError,
|
|
46
|
+
InvalidCredentialsError,
|
|
47
|
+
)
|
|
48
|
+
from .passwords import PasswordHasher
|
|
49
|
+
from .tokens import (
|
|
50
|
+
ACCESS_TOKEN_LIFETIME,
|
|
51
|
+
BEARER_TOKEN_TYPE,
|
|
52
|
+
JwtTokenService,
|
|
53
|
+
hash_refresh_token,
|
|
54
|
+
new_refresh_token,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
FAILED_ATTEMPT_LIMIT = 5
|
|
58
|
+
LOCKOUT_DURATION = timedelta(minutes=15)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@authorize()
|
|
62
|
+
@application_service(provides=AuthenticationApplicationService)
|
|
63
|
+
class _AuthenticationApplicationService(ApplicationService):
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
login_store: IdentityLoginStore,
|
|
67
|
+
sessions: RefreshSessionStore,
|
|
68
|
+
passwords: PasswordHasher,
|
|
69
|
+
tokens: JwtTokenService,
|
|
70
|
+
current_user: CurrentUser,
|
|
71
|
+
) -> None:
|
|
72
|
+
self._login_store = login_store
|
|
73
|
+
self._sessions = sessions
|
|
74
|
+
self._passwords = passwords
|
|
75
|
+
self._tokens = tokens
|
|
76
|
+
self._current_user = current_user
|
|
77
|
+
|
|
78
|
+
@allow_anonymous
|
|
79
|
+
async def login(self, command: LoginCommand) -> TokenPair:
|
|
80
|
+
now = datetime.now(UTC)
|
|
81
|
+
user = await self._login_store.find_user(_normalize_name(command.username, role="username"))
|
|
82
|
+
supplied_password = command.password.get_secret_value()
|
|
83
|
+
verified, updated_hash = await self._passwords.verify(
|
|
84
|
+
supplied_password,
|
|
85
|
+
user.password_hash if user is not None else None,
|
|
86
|
+
)
|
|
87
|
+
if user is None:
|
|
88
|
+
raise InvalidCredentialsError
|
|
89
|
+
if (
|
|
90
|
+
not user.is_active
|
|
91
|
+
or (user.lockout_end is not None and user.lockout_end > now)
|
|
92
|
+
or not verified
|
|
93
|
+
):
|
|
94
|
+
if user.is_active and (user.lockout_end is None or user.lockout_end <= now):
|
|
95
|
+
await self._login_store.record_failure(
|
|
96
|
+
user.user_id,
|
|
97
|
+
now=now,
|
|
98
|
+
attempt_limit=FAILED_ATTEMPT_LIMIT,
|
|
99
|
+
lockout_duration_seconds=int(LOCKOUT_DURATION.total_seconds()),
|
|
100
|
+
)
|
|
101
|
+
raise InvalidCredentialsError
|
|
102
|
+
|
|
103
|
+
refresh_token = new_refresh_token()
|
|
104
|
+
session = _new_session(user.user_id, refresh_token, now=now)
|
|
105
|
+
completed = await self._login_store.complete_success(
|
|
106
|
+
user.user_id,
|
|
107
|
+
expected_password_hash=user.password_hash,
|
|
108
|
+
updated_password_hash=updated_hash,
|
|
109
|
+
session=session,
|
|
110
|
+
now=now,
|
|
111
|
+
)
|
|
112
|
+
if not completed:
|
|
113
|
+
raise InvalidCredentialsError
|
|
114
|
+
return self._token_pair(session, refresh_token, now=now)
|
|
115
|
+
|
|
116
|
+
@allow_anonymous
|
|
117
|
+
async def refresh(self, command: RefreshTokenCommand) -> TokenPair:
|
|
118
|
+
now = datetime.now(UTC)
|
|
119
|
+
refresh_token = new_refresh_token()
|
|
120
|
+
result = await self._sessions.rotate(
|
|
121
|
+
hash_refresh_token(command.refresh_token.get_secret_value()),
|
|
122
|
+
replacement_id=uuid4(),
|
|
123
|
+
replacement_hash=hash_refresh_token(refresh_token),
|
|
124
|
+
now=now,
|
|
125
|
+
)
|
|
126
|
+
if result.status is not RefreshRotationStatus.ROTATED or result.session is None:
|
|
127
|
+
raise InvalidCredentialsError
|
|
128
|
+
return self._token_pair(result.session, refresh_token, now=now)
|
|
129
|
+
|
|
130
|
+
@allow_anonymous
|
|
131
|
+
async def revoke(self, command: RevokeTokenCommand) -> None:
|
|
132
|
+
await self._sessions.revoke(
|
|
133
|
+
hash_refresh_token(command.refresh_token.get_secret_value()),
|
|
134
|
+
now=datetime.now(UTC),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
async def me(self) -> CurrentUserView:
|
|
138
|
+
if (
|
|
139
|
+
self._current_user.user_id is None
|
|
140
|
+
or self._current_user.session_id is None
|
|
141
|
+
or self._current_user.username is None
|
|
142
|
+
):
|
|
143
|
+
# Invocation authorization normally rejects first;此 guard 保持 implementation 完整。
|
|
144
|
+
raise InvalidCredentialsError
|
|
145
|
+
return CurrentUserView(
|
|
146
|
+
user_id=self._current_user.user_id,
|
|
147
|
+
session_id=self._current_user.session_id,
|
|
148
|
+
username=self._current_user.username,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _token_pair(
|
|
152
|
+
self,
|
|
153
|
+
session: RefreshSession,
|
|
154
|
+
refresh_token: str,
|
|
155
|
+
*,
|
|
156
|
+
now: datetime,
|
|
157
|
+
) -> TokenPair:
|
|
158
|
+
return TokenPair(
|
|
159
|
+
access_token=self._tokens.issue(session.user_id, session.session_id, now=now),
|
|
160
|
+
refresh_token=refresh_token,
|
|
161
|
+
token_type=BEARER_TOKEN_TYPE,
|
|
162
|
+
expires_in=int(ACCESS_TOKEN_LIFETIME.total_seconds()),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@application_service(provides=IdentityManagementApplicationService)
|
|
167
|
+
class _IdentityManagementApplicationService(ApplicationService):
|
|
168
|
+
def __init__(
|
|
169
|
+
self,
|
|
170
|
+
repository: IdentityRepository,
|
|
171
|
+
passwords: PasswordHasher,
|
|
172
|
+
permissions: PermissionCatalog,
|
|
173
|
+
) -> None:
|
|
174
|
+
self._repository = repository
|
|
175
|
+
self._passwords = passwords
|
|
176
|
+
self._permissions = permissions
|
|
177
|
+
|
|
178
|
+
@authorize(IDENTITY_USERS_MANAGE)
|
|
179
|
+
async def create_user(self, command: CreateUserCommand) -> UserView:
|
|
180
|
+
normalized = _normalize_name(command.username, role="username")
|
|
181
|
+
if await self._repository.find_user_by_normalized_username(normalized) is not None:
|
|
182
|
+
raise IdentityConflictError(resource="user")
|
|
183
|
+
user = IdentityUser(
|
|
184
|
+
user_id=uuid4(),
|
|
185
|
+
username=command.username.strip(),
|
|
186
|
+
normalized_username=normalized,
|
|
187
|
+
password_hash=await self._passwords.hash(command.password.get_secret_value()),
|
|
188
|
+
is_active=True,
|
|
189
|
+
failed_access_count=0,
|
|
190
|
+
lockout_end=None,
|
|
191
|
+
permission_version=0,
|
|
192
|
+
)
|
|
193
|
+
await self._repository.create_user(user)
|
|
194
|
+
return UserView(user.user_id, user.username, user.is_active, ())
|
|
195
|
+
|
|
196
|
+
@authorize(IDENTITY_USERS_MANAGE)
|
|
197
|
+
async def get_users(self) -> tuple[UserView, ...]:
|
|
198
|
+
return await self._repository.list_users()
|
|
199
|
+
|
|
200
|
+
@authorize(IDENTITY_ROLES_MANAGE)
|
|
201
|
+
async def create_role(self, command: CreateRoleCommand) -> RoleView:
|
|
202
|
+
normalized = _normalize_name(command.name, role="role name")
|
|
203
|
+
if await self._repository.find_role_by_normalized_name(normalized) is not None:
|
|
204
|
+
raise IdentityConflictError(resource="role")
|
|
205
|
+
role = IdentityRole(uuid4(), command.name.strip(), normalized, 0)
|
|
206
|
+
await self._repository.create_role(role)
|
|
207
|
+
return RoleView(role.role_id, role.name, ())
|
|
208
|
+
|
|
209
|
+
@authorize(IDENTITY_ROLES_MANAGE)
|
|
210
|
+
async def get_roles(self) -> tuple[RoleView, ...]:
|
|
211
|
+
return await self._repository.list_roles()
|
|
212
|
+
|
|
213
|
+
@authorize(IDENTITY_USERS_MANAGE, IDENTITY_ROLES_MANAGE)
|
|
214
|
+
async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None:
|
|
215
|
+
role_ids = tuple(dict.fromkeys(command.role_ids))
|
|
216
|
+
await self._repository.set_user_roles(user_id, role_ids)
|
|
217
|
+
|
|
218
|
+
@authorize(IDENTITY_ROLES_MANAGE)
|
|
219
|
+
async def set_role_permissions(self, role_id: UUID, command: SetRolePermissionsCommand) -> None:
|
|
220
|
+
names = tuple(PermissionName(item) for item in command.permissions)
|
|
221
|
+
if len(set(names)) != len(names):
|
|
222
|
+
raise IdentityConflictError(resource="role permission")
|
|
223
|
+
unknown = tuple(item for item in names if item not in self._permissions)
|
|
224
|
+
if unknown:
|
|
225
|
+
raise IdentityResourceNotFoundError(resource="permission")
|
|
226
|
+
await self._repository.set_role_permissions(
|
|
227
|
+
role_id,
|
|
228
|
+
tuple(item.value for item in names),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@application_service()
|
|
233
|
+
class IdentityAdminSeedContributor(DataSeedContributor):
|
|
234
|
+
def __init__(
|
|
235
|
+
self,
|
|
236
|
+
repository: IdentityRepository,
|
|
237
|
+
passwords: PasswordHasher,
|
|
238
|
+
permissions: PermissionCatalog,
|
|
239
|
+
options: Options[IdentityOptions],
|
|
240
|
+
) -> None:
|
|
241
|
+
self._repository = repository
|
|
242
|
+
self._passwords = passwords
|
|
243
|
+
self._permissions = permissions
|
|
244
|
+
self._options = options.value
|
|
245
|
+
|
|
246
|
+
async def seed(self) -> None:
|
|
247
|
+
username = self._options.seed_admin_username
|
|
248
|
+
password = self._options.seed_admin_password
|
|
249
|
+
if username is None or password is None:
|
|
250
|
+
return
|
|
251
|
+
normalized_username = _normalize_name(username, role="seed administrator username")
|
|
252
|
+
user = await self._repository.find_user_by_normalized_username(normalized_username)
|
|
253
|
+
if user is None:
|
|
254
|
+
user = IdentityUser(
|
|
255
|
+
uuid4(),
|
|
256
|
+
username.strip(),
|
|
257
|
+
normalized_username,
|
|
258
|
+
await self._passwords.hash(password.get_secret_value()),
|
|
259
|
+
True,
|
|
260
|
+
0,
|
|
261
|
+
None,
|
|
262
|
+
0,
|
|
263
|
+
)
|
|
264
|
+
await self._repository.create_user(user)
|
|
265
|
+
normalized_role = "administrator"
|
|
266
|
+
role = await self._repository.find_role_by_normalized_name(normalized_role)
|
|
267
|
+
if role is None:
|
|
268
|
+
role = IdentityRole(uuid4(), "Administrator", normalized_role, 0)
|
|
269
|
+
await self._repository.create_role(role)
|
|
270
|
+
await self._repository.set_role_permissions(
|
|
271
|
+
role.role_id,
|
|
272
|
+
tuple(item.name.value for item in self._permissions.definitions),
|
|
273
|
+
)
|
|
274
|
+
await self._repository.set_user_roles(user.user_id, (role.role_id,))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _new_session(user_id: UUID, refresh_token: str, *, now: datetime) -> RefreshSession:
|
|
278
|
+
family_id = uuid4()
|
|
279
|
+
return RefreshSession(
|
|
280
|
+
session_id=uuid4(),
|
|
281
|
+
user_id=user_id,
|
|
282
|
+
family_id=family_id,
|
|
283
|
+
token_hash=hash_refresh_token(refresh_token),
|
|
284
|
+
created_at=now,
|
|
285
|
+
family_created_at=now,
|
|
286
|
+
last_used_at=now,
|
|
287
|
+
)
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Identity 的 HTTP/Application contracts、持久化值与 Host keyring Options。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Annotated, Protocol
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
|
|
13
|
+
|
|
14
|
+
from ..application_services import ApplicationServiceContract
|
|
15
|
+
from ..authorization import (
|
|
16
|
+
CurrentUser,
|
|
17
|
+
PermissionName,
|
|
18
|
+
RolePermissionVersion,
|
|
19
|
+
)
|
|
20
|
+
from ..options import BaseOptions
|
|
21
|
+
|
|
22
|
+
IDENTITY_USERS_MANAGE = PermissionName("identity.users.manage")
|
|
23
|
+
IDENTITY_ROLES_MANAGE = PermissionName("identity.roles.manage")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IdentityOptions(BaseOptions):
|
|
27
|
+
issuer: str
|
|
28
|
+
audience: str
|
|
29
|
+
active_kid: str
|
|
30
|
+
keys: Mapping[str, SecretStr]
|
|
31
|
+
seed_admin_username: str | None = None
|
|
32
|
+
seed_admin_password: SecretStr | None = None
|
|
33
|
+
|
|
34
|
+
@field_validator("issuer", "audience", "active_kid")
|
|
35
|
+
@classmethod
|
|
36
|
+
def _validate_identifier(cls, value: str) -> str:
|
|
37
|
+
if not value or value != value.strip():
|
|
38
|
+
raise ValueError("JWT identifier must be a non-empty trimmed string")
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
@field_validator("keys")
|
|
42
|
+
@classmethod
|
|
43
|
+
def _validate_keys(cls, value: Mapping[str, SecretStr]) -> Mapping[str, SecretStr]:
|
|
44
|
+
if not value:
|
|
45
|
+
raise ValueError("JWT keyring must contain at least one key")
|
|
46
|
+
for kid, secret in value.items():
|
|
47
|
+
if not kid or kid != kid.strip():
|
|
48
|
+
raise ValueError("JWT kid must be a non-empty trimmed string")
|
|
49
|
+
if len(secret.get_secret_value().encode("utf-8")) < 32:
|
|
50
|
+
raise ValueError("HS256 keys must contain at least 256 bits")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
@model_validator(mode="after")
|
|
54
|
+
def _validate_active_and_seed(self) -> IdentityOptions:
|
|
55
|
+
if self.active_kid not in self.keys:
|
|
56
|
+
raise ValueError("active_kid must reference the JWT keyring")
|
|
57
|
+
if (self.seed_admin_username is None) != (self.seed_admin_password is None):
|
|
58
|
+
raise ValueError("seed administrator username and password must be configured together")
|
|
59
|
+
if self.seed_admin_username is not None:
|
|
60
|
+
_normalize_name(self.seed_admin_username, role="seed administrator username")
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LoginCommand(BaseModel):
|
|
65
|
+
model_config = ConfigDict(frozen=True)
|
|
66
|
+
|
|
67
|
+
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
68
|
+
password: SecretStr
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RefreshTokenCommand(BaseModel):
|
|
72
|
+
model_config = ConfigDict(frozen=True)
|
|
73
|
+
|
|
74
|
+
refresh_token: SecretStr
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RevokeTokenCommand(BaseModel):
|
|
78
|
+
model_config = ConfigDict(frozen=True)
|
|
79
|
+
|
|
80
|
+
refresh_token: SecretStr
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CreateUserCommand(BaseModel):
|
|
84
|
+
model_config = ConfigDict(frozen=True)
|
|
85
|
+
|
|
86
|
+
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
87
|
+
password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CreateRoleCommand(BaseModel):
|
|
91
|
+
model_config = ConfigDict(frozen=True)
|
|
92
|
+
|
|
93
|
+
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SetUserRolesCommand(BaseModel):
|
|
97
|
+
model_config = ConfigDict(frozen=True)
|
|
98
|
+
|
|
99
|
+
role_ids: tuple[UUID, ...]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SetRolePermissionsCommand(BaseModel):
|
|
103
|
+
model_config = ConfigDict(frozen=True)
|
|
104
|
+
|
|
105
|
+
permissions: tuple[str, ...]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass(frozen=True, slots=True)
|
|
109
|
+
class TokenPair:
|
|
110
|
+
access_token: str
|
|
111
|
+
refresh_token: str
|
|
112
|
+
token_type: str
|
|
113
|
+
expires_in: int
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True, slots=True)
|
|
117
|
+
class UserView:
|
|
118
|
+
user_id: UUID
|
|
119
|
+
username: str
|
|
120
|
+
is_active: bool
|
|
121
|
+
role_ids: tuple[UUID, ...]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class RoleView:
|
|
126
|
+
role_id: UUID
|
|
127
|
+
name: str
|
|
128
|
+
permissions: tuple[str, ...]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True, slots=True)
|
|
132
|
+
class CurrentUserView:
|
|
133
|
+
user_id: UUID
|
|
134
|
+
session_id: UUID
|
|
135
|
+
username: str
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True, slots=True)
|
|
139
|
+
class IdentityUser:
|
|
140
|
+
user_id: UUID
|
|
141
|
+
username: str
|
|
142
|
+
normalized_username: str
|
|
143
|
+
password_hash: str
|
|
144
|
+
is_active: bool
|
|
145
|
+
failed_access_count: int
|
|
146
|
+
lockout_end: datetime | None
|
|
147
|
+
permission_version: int
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass(frozen=True, slots=True)
|
|
151
|
+
class IdentityRole:
|
|
152
|
+
role_id: UUID
|
|
153
|
+
name: str
|
|
154
|
+
normalized_name: str
|
|
155
|
+
permission_version: int
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@dataclass(frozen=True, slots=True)
|
|
159
|
+
class RefreshSession:
|
|
160
|
+
session_id: UUID
|
|
161
|
+
user_id: UUID
|
|
162
|
+
family_id: UUID
|
|
163
|
+
token_hash: str
|
|
164
|
+
created_at: datetime
|
|
165
|
+
family_created_at: datetime
|
|
166
|
+
last_used_at: datetime
|
|
167
|
+
revoked_at: datetime | None = None
|
|
168
|
+
replaced_by_session_id: UUID | None = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class RefreshRotationStatus(Enum):
|
|
172
|
+
ROTATED = "rotated"
|
|
173
|
+
NOT_FOUND = "not_found"
|
|
174
|
+
REUSED = "reused"
|
|
175
|
+
EXPIRED = "expired"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(frozen=True, slots=True)
|
|
179
|
+
class RefreshRotationResult:
|
|
180
|
+
status: RefreshRotationStatus
|
|
181
|
+
session: RefreshSession | None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class IdentityRepository(Protocol):
|
|
185
|
+
async def find_user_by_normalized_username(
|
|
186
|
+
self, normalized_username: str
|
|
187
|
+
) -> IdentityUser | None: ...
|
|
188
|
+
|
|
189
|
+
async def create_user(self, user: IdentityUser) -> None: ...
|
|
190
|
+
|
|
191
|
+
async def list_users(self) -> tuple[UserView, ...]: ...
|
|
192
|
+
|
|
193
|
+
async def create_role(self, role: IdentityRole) -> None: ...
|
|
194
|
+
|
|
195
|
+
async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None: ...
|
|
196
|
+
|
|
197
|
+
async def list_roles(self) -> tuple[RoleView, ...]: ...
|
|
198
|
+
|
|
199
|
+
async def set_user_roles(self, user_id: UUID, role_ids: tuple[UUID, ...]) -> None: ...
|
|
200
|
+
|
|
201
|
+
async def set_role_permissions(self, role_id: UUID, permissions: tuple[str, ...]) -> None: ...
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class IdentityAccessStore(Protocol):
|
|
205
|
+
async def load_current_user(
|
|
206
|
+
self,
|
|
207
|
+
user_id: UUID,
|
|
208
|
+
session_id: UUID,
|
|
209
|
+
now: datetime,
|
|
210
|
+
) -> CurrentUser | None: ...
|
|
211
|
+
|
|
212
|
+
async def permissions_for_roles(
|
|
213
|
+
self, role_versions: tuple[RolePermissionVersion, ...]
|
|
214
|
+
) -> frozenset[str]: ...
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class IdentityLoginStore(Protocol):
|
|
218
|
+
async def find_user(self, normalized_username: str) -> IdentityUser | None: ...
|
|
219
|
+
|
|
220
|
+
async def record_failure(
|
|
221
|
+
self,
|
|
222
|
+
user_id: UUID,
|
|
223
|
+
*,
|
|
224
|
+
now: datetime,
|
|
225
|
+
attempt_limit: int,
|
|
226
|
+
lockout_duration_seconds: int,
|
|
227
|
+
) -> None: ...
|
|
228
|
+
|
|
229
|
+
async def complete_success(
|
|
230
|
+
self,
|
|
231
|
+
user_id: UUID,
|
|
232
|
+
*,
|
|
233
|
+
expected_password_hash: str,
|
|
234
|
+
updated_password_hash: str | None,
|
|
235
|
+
session: RefreshSession,
|
|
236
|
+
now: datetime,
|
|
237
|
+
) -> bool: ...
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class RefreshSessionStore(Protocol):
|
|
241
|
+
async def rotate(
|
|
242
|
+
self,
|
|
243
|
+
token_hash: str,
|
|
244
|
+
*,
|
|
245
|
+
replacement_id: UUID,
|
|
246
|
+
replacement_hash: str,
|
|
247
|
+
now: datetime,
|
|
248
|
+
) -> RefreshRotationResult: ...
|
|
249
|
+
|
|
250
|
+
async def revoke(self, token_hash: str, *, now: datetime) -> None: ...
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class AuthenticationApplicationService(ApplicationServiceContract, Protocol):
|
|
254
|
+
async def login(self, command: LoginCommand) -> TokenPair: ...
|
|
255
|
+
|
|
256
|
+
async def refresh(self, command: RefreshTokenCommand) -> TokenPair: ...
|
|
257
|
+
|
|
258
|
+
async def revoke(self, command: RevokeTokenCommand) -> None: ...
|
|
259
|
+
|
|
260
|
+
async def me(self) -> CurrentUserView: ...
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class IdentityManagementApplicationService(ApplicationServiceContract, Protocol):
|
|
264
|
+
async def create_user(self, command: CreateUserCommand) -> UserView: ...
|
|
265
|
+
|
|
266
|
+
async def get_users(self) -> tuple[UserView, ...]: ...
|
|
267
|
+
|
|
268
|
+
async def create_role(self, command: CreateRoleCommand) -> RoleView: ...
|
|
269
|
+
|
|
270
|
+
async def get_roles(self) -> tuple[RoleView, ...]: ...
|
|
271
|
+
|
|
272
|
+
async def set_user_roles(self, user_id: UUID, command: SetUserRolesCommand) -> None: ...
|
|
273
|
+
|
|
274
|
+
async def set_role_permissions(
|
|
275
|
+
self, role_id: UUID, command: SetRolePermissionsCommand
|
|
276
|
+
) -> None: ...
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _normalize_name(value: str, *, role: str) -> str:
|
|
280
|
+
normalized = value.strip().casefold()
|
|
281
|
+
if not normalized or len(normalized) > 120:
|
|
282
|
+
raise ValueError(f"{role} must contain between 1 and 120 characters")
|
|
283
|
+
return normalized
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Identity 对外只暴露静态错误类型,不泄漏凭据、Token 或数据库文本。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..authorization import UnauthenticatedError
|
|
6
|
+
from ..errors.base import FrameworkError, ResourceConflictError, ResourceNotFoundError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IdentityDefinitionError(FrameworkError):
|
|
10
|
+
def __init__(self, *, reason: str) -> None:
|
|
11
|
+
self.reason = reason
|
|
12
|
+
super().__init__(f"Invalid Identity configuration: {reason}")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InvalidCredentialsError(UnauthenticatedError):
|
|
16
|
+
"""用户名不存在、密码错误、锁定和 Token 无效共享同一公开失败。"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IdentityConflictError(ResourceConflictError):
|
|
20
|
+
def __init__(self, *, resource: str) -> None:
|
|
21
|
+
self.resource = resource
|
|
22
|
+
super().__init__(f"Identity resource {resource!r} already exists")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IdentityResourceNotFoundError(ResourceNotFoundError):
|
|
26
|
+
def __init__(self, *, resource: str) -> None:
|
|
27
|
+
self.resource = resource
|
|
28
|
+
super().__init__(f"Identity resource {resource!r} was not found")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Identity Application Services 的标准 HTTP 路径。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..fastapi import ApplicationServiceRouteOverride, exposes_application_services
|
|
6
|
+
from ..modularity import AppModule
|
|
7
|
+
from .contracts import AuthenticationApplicationService, IdentityManagementApplicationService
|
|
8
|
+
from .module import IdentityModule
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@exposes_application_services(
|
|
12
|
+
IdentityModule,
|
|
13
|
+
root_path="identity",
|
|
14
|
+
overrides=(
|
|
15
|
+
ApplicationServiceRouteOverride(
|
|
16
|
+
AuthenticationApplicationService,
|
|
17
|
+
method=AuthenticationApplicationService.login,
|
|
18
|
+
path="/api/auth/login",
|
|
19
|
+
verb="POST",
|
|
20
|
+
operation_id="identity_login",
|
|
21
|
+
),
|
|
22
|
+
ApplicationServiceRouteOverride(
|
|
23
|
+
AuthenticationApplicationService,
|
|
24
|
+
method=AuthenticationApplicationService.refresh,
|
|
25
|
+
path="/api/auth/refresh",
|
|
26
|
+
verb="POST",
|
|
27
|
+
operation_id="identity_refresh",
|
|
28
|
+
),
|
|
29
|
+
ApplicationServiceRouteOverride(
|
|
30
|
+
AuthenticationApplicationService,
|
|
31
|
+
method=AuthenticationApplicationService.revoke,
|
|
32
|
+
path="/api/auth/revoke",
|
|
33
|
+
verb="POST",
|
|
34
|
+
operation_id="identity_revoke",
|
|
35
|
+
),
|
|
36
|
+
ApplicationServiceRouteOverride(
|
|
37
|
+
AuthenticationApplicationService,
|
|
38
|
+
method=AuthenticationApplicationService.me,
|
|
39
|
+
path="/api/auth/me",
|
|
40
|
+
verb="GET",
|
|
41
|
+
operation_id="identity_me",
|
|
42
|
+
),
|
|
43
|
+
ApplicationServiceRouteOverride(
|
|
44
|
+
IdentityManagementApplicationService,
|
|
45
|
+
method=IdentityManagementApplicationService.create_user,
|
|
46
|
+
path="/api/identity/users",
|
|
47
|
+
verb="POST",
|
|
48
|
+
status_code=201,
|
|
49
|
+
operation_id="identity_create_user",
|
|
50
|
+
),
|
|
51
|
+
ApplicationServiceRouteOverride(
|
|
52
|
+
IdentityManagementApplicationService,
|
|
53
|
+
method=IdentityManagementApplicationService.get_users,
|
|
54
|
+
path="/api/identity/users",
|
|
55
|
+
verb="GET",
|
|
56
|
+
operation_id="identity_get_users",
|
|
57
|
+
),
|
|
58
|
+
ApplicationServiceRouteOverride(
|
|
59
|
+
IdentityManagementApplicationService,
|
|
60
|
+
method=IdentityManagementApplicationService.create_role,
|
|
61
|
+
path="/api/identity/roles",
|
|
62
|
+
verb="POST",
|
|
63
|
+
status_code=201,
|
|
64
|
+
operation_id="identity_create_role",
|
|
65
|
+
),
|
|
66
|
+
ApplicationServiceRouteOverride(
|
|
67
|
+
IdentityManagementApplicationService,
|
|
68
|
+
method=IdentityManagementApplicationService.get_roles,
|
|
69
|
+
path="/api/identity/roles",
|
|
70
|
+
verb="GET",
|
|
71
|
+
operation_id="identity_get_roles",
|
|
72
|
+
),
|
|
73
|
+
ApplicationServiceRouteOverride(
|
|
74
|
+
IdentityManagementApplicationService,
|
|
75
|
+
method=IdentityManagementApplicationService.set_user_roles,
|
|
76
|
+
path="/api/identity/users/{user_id}/roles",
|
|
77
|
+
verb="PUT",
|
|
78
|
+
operation_id="identity_set_user_roles",
|
|
79
|
+
),
|
|
80
|
+
ApplicationServiceRouteOverride(
|
|
81
|
+
IdentityManagementApplicationService,
|
|
82
|
+
method=IdentityManagementApplicationService.set_role_permissions,
|
|
83
|
+
path="/api/identity/roles/{role_id}/permissions",
|
|
84
|
+
verb="PUT",
|
|
85
|
+
operation_id="identity_set_role_permissions",
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
class IdentityHttpApiModule(AppModule):
|
|
90
|
+
dependencies = (IdentityModule,)
|