python-ddd-framework 0.4.0__py3-none-any.whl → 0.6.0__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_ddd_framework/application/runtime.py +46 -19
- python_ddd_framework/application_services/bindings.py +0 -64
- python_ddd_framework/application_services/dispatcher.py +1 -1
- python_ddd_framework/application_services/invocation.py +7 -500
- python_ddd_framework/application_services/module.py +16 -5
- python_ddd_framework/authorization/module.py +1 -1
- python_ddd_framework/background_execution/local.py +20 -1
- python_ddd_framework/background_execution/processes.py +11 -1
- python_ddd_framework/background_jobs/execution.py +1 -1
- python_ddd_framework/background_jobs/pgqueuer/enqueue.py +1 -1
- python_ddd_framework/background_workers/execution.py +1 -1
- python_ddd_framework/background_workers/runtime.py +11 -2
- python_ddd_framework/caching/unit_of_work.py +2 -4
- python_ddd_framework/cli/project.py +11 -10
- python_ddd_framework/developer_kit/generation.py +46 -36
- python_ddd_framework/developer_kit/publication.py +442 -0
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +42 -3
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_integration_service.py.jinja +31 -7
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/hosted_services/order_observation_handler.py.jinja +2 -2
- python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/order_repository.py.jinja +3 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +21 -3
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +18 -1
- python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +74 -3
- python_ddd_framework/diagnostics/source.py +14 -1
- python_ddd_framework/domain/aggregates.py +5 -12
- python_ddd_framework/domain/event_values.py +124 -0
- python_ddd_framework/events/runtime.py +2 -7
- python_ddd_framework/events/unit_of_work.py +1 -4
- python_ddd_framework/fastapi/action.py +2 -1
- python_ddd_framework/fastapi/adapter.py +11 -117
- python_ddd_framework/fastapi/application_services.py +17 -402
- python_ddd_framework/fastapi/error_handlers.py +128 -0
- python_ddd_framework/fastapi/realtime/runtime.py +1 -1
- python_ddd_framework/fastapi/request_context.py +2 -5
- python_ddd_framework/fastapi/routing.py +4 -10
- python_ddd_framework/fastapi/service_endpoints.py +182 -0
- python_ddd_framework/fastapi/service_routes.py +237 -0
- python_ddd_framework/fastapi/transfer.py +1 -1
- python_ddd_framework/hosted_services/bridge.py +76 -21
- python_ddd_framework/hosted_services/contracts.py +5 -1
- python_ddd_framework/hosted_services/runtime.py +71 -17
- python_ddd_framework/identity/__init__.py +21 -13
- python_ddd_framework/identity/application.py +12 -66
- python_ddd_framework/identity/contracts.py +38 -411
- python_ddd_framework/identity/domain_rules.py +17 -0
- python_ddd_framework/identity/dtos.py +152 -0
- python_ddd_framework/identity/extensions.py +9 -0
- python_ddd_framework/identity/management.py +28 -25
- python_ddd_framework/identity/models.py +86 -0
- python_ddd_framework/identity/module.py +3 -4
- python_ddd_framework/identity/options.py +54 -0
- python_ddd_framework/identity/seeding.py +59 -0
- python_ddd_framework/identity/services.py +1 -1
- python_ddd_framework/identity/sqlalchemy/module.py +9 -6
- python_ddd_framework/identity/sqlalchemy/repository.py +4 -5
- python_ddd_framework/identity/sqlalchemy/session_security.py +1 -3
- python_ddd_framework/identity/sqlalchemy/stores.py +35 -47
- python_ddd_framework/identity/stores.py +120 -0
- python_ddd_framework/identity/tokens.py +1 -1
- python_ddd_framework/invocation/callables.py +1 -1
- python_ddd_framework/invocation/dispatcher.py +1 -1
- python_ddd_framework/invocation/entrypoints.py +2 -1
- python_ddd_framework/invocation/function_runtime.py +2 -2
- python_ddd_framework/invocation/interception.py +1 -1
- python_ddd_framework/invocation/managed_proxy.py +2 -1
- python_ddd_framework/invocation/managed_services.py +8 -14
- python_ddd_framework/invocation/methods.py +2 -7
- python_ddd_framework/invocation/module.py +1 -1
- python_ddd_framework/invocation/runtime.py +483 -0
- python_ddd_framework/invocation/scopes.py +91 -0
- python_ddd_framework/lifecycle/participants.py +4 -0
- python_ddd_framework/messaging/channel.py +35 -8
- python_ddd_framework/messaging/contracts.py +9 -0
- python_ddd_framework/messaging/module.py +13 -0
- python_ddd_framework/messaging/runtime.py +17 -2
- python_ddd_framework/modularity/discovery.py +2 -7
- python_ddd_framework/modularity/graph.py +2 -8
- python_ddd_framework/observability/logging.py +1 -1
- python_ddd_framework/observability/tracing.py +1 -1
- python_ddd_framework/redis/cache.py +2 -0
- python_ddd_framework/redis/notification_runtime.py +1 -1
- python_ddd_framework/services/arbitration.py +1 -20
- python_ddd_framework/services/composition.py +71 -2
- python_ddd_framework/services/provider.py +23 -1
- python_ddd_framework/services/relocation.py +96 -0
- python_ddd_framework/settings/refresh.py +1 -1
- python_ddd_framework/settings/sqlalchemy/store.py +1 -1
- python_ddd_framework/sqlalchemy/metadata.py +11 -413
- python_ddd_framework/sqlalchemy/metadata_builder.py +234 -0
- python_ddd_framework/sqlalchemy/metadata_fingerprint.py +108 -0
- python_ddd_framework/sqlalchemy/metadata_ownership.py +73 -0
- python_ddd_framework/sqlalchemy/migration.py +2 -1
- python_ddd_framework/sqlalchemy/migration_sources.py +38 -0
- python_ddd_framework/sqlalchemy/module.py +2 -1
- python_ddd_framework/sqlalchemy/unit_of_work.py +1 -1
- python_ddd_framework/unit_of_work/contracts.py +68 -7
- python_ddd_framework/unit_of_work/manager.py +4 -7
- python_ddd_framework/unit_of_work/module.py +1 -1
- {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/METADATA +82 -4
- {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/RECORD +104 -85
- {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/WHEEL +1 -1
- {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/entry_points.txt +0 -0
- {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {python_ddd_framework-0.4.0.dist-info → python_ddd_framework-0.6.0.dist-info}/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +0 -0
|
@@ -1,448 +1,75 @@
|
|
|
1
|
-
"""Identity
|
|
1
|
+
"""Identity 应用服务契约与所属权限名称。"""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from dataclasses import dataclass, field, replace
|
|
7
|
-
from datetime import datetime, timedelta
|
|
8
|
-
from enum import Enum
|
|
9
|
-
from typing import Annotated, Protocol
|
|
5
|
+
from typing import Protocol
|
|
10
6
|
from uuid import UUID
|
|
11
7
|
|
|
12
|
-
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator, model_validator
|
|
13
|
-
|
|
14
8
|
from ..application_services import ApplicationServiceContract, PagedResult, PagedResultRequestDto
|
|
15
|
-
from ..authorization import
|
|
16
|
-
|
|
17
|
-
PermissionName,
|
|
18
|
-
RolePermissionVersion,
|
|
19
|
-
)
|
|
20
|
-
from ..domain import OptimisticConcurrencyError
|
|
21
|
-
from ..extensions.contracts import ExtensibleModel, ExtensionProperties, ExtensionTarget
|
|
22
|
-
from ..options import BaseOptions
|
|
9
|
+
from ..authorization import PermissionName
|
|
10
|
+
from . import dtos
|
|
23
11
|
|
|
24
12
|
IDENTITY_USERS_MANAGE = PermissionName("identity.users.manage")
|
|
25
|
-
IDENTITY_ROLES_MANAGE = PermissionName("identity.roles.manage")
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class IdentityOptions(BaseOptions):
|
|
29
|
-
issuer: str
|
|
30
|
-
audience: str
|
|
31
|
-
active_kid: str
|
|
32
|
-
keys: Mapping[str, SecretStr]
|
|
33
|
-
access_token_lifetime: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
|
|
34
|
-
refresh_inactivity_lifetime: timedelta = Field(default=timedelta(days=30), gt=timedelta(0))
|
|
35
|
-
refresh_absolute_lifetime: timedelta = Field(default=timedelta(days=90), gt=timedelta(0))
|
|
36
|
-
failed_attempt_limit: int = Field(default=5, ge=1)
|
|
37
|
-
lockout_duration: timedelta = Field(default=timedelta(minutes=15), ge=timedelta(seconds=1))
|
|
38
|
-
seed_admin_username: str | None = None
|
|
39
|
-
seed_admin_password: SecretStr | None = None
|
|
40
|
-
|
|
41
|
-
@field_validator("issuer", "audience", "active_kid")
|
|
42
|
-
@classmethod
|
|
43
|
-
def _validate_identifier(cls, value: str) -> str:
|
|
44
|
-
if not value or value != value.strip():
|
|
45
|
-
raise ValueError("JWT identifier must be a non-empty trimmed string")
|
|
46
|
-
return value
|
|
47
|
-
|
|
48
|
-
@field_validator("keys")
|
|
49
|
-
@classmethod
|
|
50
|
-
def _validate_keys(cls, value: Mapping[str, SecretStr]) -> Mapping[str, SecretStr]:
|
|
51
|
-
if not value:
|
|
52
|
-
raise ValueError("JWT keyring must contain at least one key")
|
|
53
|
-
for kid, secret in value.items():
|
|
54
|
-
if not kid or kid != kid.strip():
|
|
55
|
-
raise ValueError("JWT kid must be a non-empty trimmed string")
|
|
56
|
-
if len(secret.get_secret_value().encode("utf-8")) < 32:
|
|
57
|
-
raise ValueError("HS256 keys must contain at least 256 bits")
|
|
58
|
-
return value
|
|
59
|
-
|
|
60
|
-
@model_validator(mode="after")
|
|
61
|
-
def _validate_active_and_seed(self) -> IdentityOptions:
|
|
62
|
-
if self.active_kid not in self.keys:
|
|
63
|
-
raise ValueError("active_kid must reference the JWT keyring")
|
|
64
|
-
if (self.seed_admin_username is None) != (self.seed_admin_password is None):
|
|
65
|
-
raise ValueError("seed administrator username and password must be configured together")
|
|
66
|
-
if self.seed_admin_username is not None:
|
|
67
|
-
_normalize_name(self.seed_admin_username, role="seed administrator username")
|
|
68
|
-
return self
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
class LoginCommand(BaseModel):
|
|
72
|
-
model_config = ConfigDict(frozen=True)
|
|
73
|
-
|
|
74
|
-
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
75
|
-
password: SecretStr
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class RefreshTokenCommand(BaseModel):
|
|
79
|
-
model_config = ConfigDict(frozen=True)
|
|
80
|
-
|
|
81
|
-
refresh_token: SecretStr
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
class RevokeTokenCommand(BaseModel):
|
|
85
|
-
model_config = ConfigDict(frozen=True)
|
|
86
|
-
|
|
87
|
-
refresh_token: SecretStr
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
class CreateUserCommand(ExtensibleModel):
|
|
91
|
-
model_config = ConfigDict(frozen=True)
|
|
92
|
-
|
|
93
|
-
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
94
|
-
password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
95
|
-
|
|
96
|
-
@field_validator("username")
|
|
97
|
-
@classmethod
|
|
98
|
-
def _validate_username(cls, value: str) -> str:
|
|
99
|
-
_normalize_name(value, role="username")
|
|
100
|
-
return value.strip()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class CreateRoleCommand(BaseModel):
|
|
104
|
-
model_config = ConfigDict(frozen=True)
|
|
105
|
-
|
|
106
|
-
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
107
|
-
|
|
108
|
-
@field_validator("name")
|
|
109
|
-
@classmethod
|
|
110
|
-
def _validate_name(cls, value: str) -> str:
|
|
111
|
-
_normalize_name(value, role="role name")
|
|
112
|
-
return value.strip()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
class IdentityConcurrencyCommand(BaseModel):
|
|
116
|
-
model_config = ConfigDict(frozen=True)
|
|
117
|
-
|
|
118
|
-
concurrency_version: Annotated[int, Field(ge=0)]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
class UpdateUserCommand(ExtensibleModel):
|
|
122
|
-
model_config = ConfigDict(frozen=True)
|
|
123
|
-
|
|
124
|
-
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
125
|
-
concurrency_version: Annotated[int, Field(ge=0)]
|
|
126
|
-
|
|
127
|
-
@field_validator("username")
|
|
128
|
-
@classmethod
|
|
129
|
-
def _validate_username(cls, value: str) -> str:
|
|
130
|
-
_normalize_name(value, role="username")
|
|
131
|
-
return value.strip()
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class SetUserActiveCommand(IdentityConcurrencyCommand):
|
|
135
|
-
is_active: bool
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
class UpdateRoleCommand(IdentityConcurrencyCommand):
|
|
139
|
-
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
140
|
-
|
|
141
|
-
@field_validator("name")
|
|
142
|
-
@classmethod
|
|
143
|
-
def _validate_name(cls, value: str) -> str:
|
|
144
|
-
_normalize_name(value, role="role name")
|
|
145
|
-
return value.strip()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class ChangePasswordCommand(IdentityConcurrencyCommand):
|
|
149
|
-
old_password: SecretStr
|
|
150
|
-
new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class SetPasswordCommand(IdentityConcurrencyCommand):
|
|
154
|
-
new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class SetUserRolesCommand(IdentityConcurrencyCommand):
|
|
158
|
-
model_config = ConfigDict(frozen=True)
|
|
159
|
-
|
|
160
|
-
role_ids: tuple[UUID, ...]
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
class SetRolePermissionsCommand(IdentityConcurrencyCommand):
|
|
164
|
-
model_config = ConfigDict(frozen=True)
|
|
165
|
-
|
|
166
|
-
permissions: tuple[str, ...]
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@dataclass(frozen=True, slots=True)
|
|
170
|
-
class TokenPair:
|
|
171
|
-
access_token: str
|
|
172
|
-
refresh_token: str
|
|
173
|
-
token_type: str
|
|
174
|
-
expires_in: int
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class UserView(ExtensibleModel):
|
|
178
|
-
user_id: UUID
|
|
179
|
-
username: str
|
|
180
|
-
is_active: bool
|
|
181
|
-
role_ids: tuple[UUID, ...]
|
|
182
|
-
concurrency_version: int
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
@dataclass(frozen=True, slots=True)
|
|
186
|
-
class RoleView:
|
|
187
|
-
role_id: UUID
|
|
188
|
-
name: str
|
|
189
|
-
permissions: tuple[str, ...]
|
|
190
|
-
concurrency_version: int
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
class UserRolesView(BaseModel):
|
|
194
|
-
role_ids: tuple[UUID, ...]
|
|
195
|
-
concurrency_version: int
|
|
196
|
-
|
|
197
13
|
|
|
198
|
-
class RolePermissionsView(BaseModel):
|
|
199
|
-
permissions: tuple[str, ...]
|
|
200
|
-
concurrency_version: int
|
|
201
14
|
|
|
202
|
-
|
|
203
|
-
@dataclass(frozen=True, slots=True)
|
|
204
|
-
class CurrentUserView:
|
|
205
|
-
user_id: UUID
|
|
206
|
-
session_id: UUID
|
|
207
|
-
username: str
|
|
208
|
-
concurrency_version: int
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
@dataclass(frozen=True, slots=True)
|
|
212
|
-
class IdentityUser:
|
|
213
|
-
user_id: UUID
|
|
214
|
-
username: str
|
|
215
|
-
normalized_username: str
|
|
216
|
-
password_hash: str
|
|
217
|
-
is_active: bool
|
|
218
|
-
failed_access_count: int
|
|
219
|
-
lockout_end: datetime | None
|
|
220
|
-
permission_version: int
|
|
221
|
-
extra_properties: ExtensionProperties = field(default_factory=ExtensionProperties)
|
|
222
|
-
concurrency_version: int = 0
|
|
223
|
-
|
|
224
|
-
def renamed(self, username: str, expected_version: int) -> IdentityUser:
|
|
225
|
-
_check_version(self.concurrency_version, expected_version)
|
|
226
|
-
return replace(
|
|
227
|
-
self,
|
|
228
|
-
username=username.strip(),
|
|
229
|
-
normalized_username=_normalize_name(username, role="username"),
|
|
230
|
-
concurrency_version=self.concurrency_version + 1,
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
def with_active(self, is_active: bool, expected_version: int) -> IdentityUser:
|
|
234
|
-
_check_version(self.concurrency_version, expected_version)
|
|
235
|
-
return replace(
|
|
236
|
-
self,
|
|
237
|
-
is_active=is_active,
|
|
238
|
-
concurrency_version=self.concurrency_version + (self.is_active != is_active),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
IDENTITY_USER_EXTENSION = ExtensionTarget(
|
|
243
|
-
"identity.user", (CreateUserCommand, UpdateUserCommand, UserView), IdentityUser
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
@dataclass(frozen=True, slots=True)
|
|
248
|
-
class IdentityRole:
|
|
249
|
-
role_id: UUID
|
|
250
|
-
name: str
|
|
251
|
-
normalized_name: str
|
|
252
|
-
permission_version: int
|
|
253
|
-
concurrency_version: int = 0
|
|
254
|
-
|
|
255
|
-
def renamed(self, name: str, expected_version: int) -> IdentityRole:
|
|
256
|
-
_check_version(self.concurrency_version, expected_version)
|
|
257
|
-
return replace(
|
|
258
|
-
self,
|
|
259
|
-
name=name.strip(),
|
|
260
|
-
normalized_name=_normalize_name(name, role="role name"),
|
|
261
|
-
concurrency_version=self.concurrency_version + 1,
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
@dataclass(frozen=True, slots=True)
|
|
266
|
-
class RefreshSession:
|
|
267
|
-
session_id: UUID
|
|
268
|
-
user_id: UUID
|
|
269
|
-
family_id: UUID
|
|
270
|
-
token_hash: str
|
|
271
|
-
created_at: datetime
|
|
272
|
-
family_created_at: datetime
|
|
273
|
-
last_used_at: datetime
|
|
274
|
-
revoked_at: datetime | None = None
|
|
275
|
-
replaced_by_session_id: UUID | None = None
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
class RefreshRotationStatus(Enum):
|
|
279
|
-
ROTATED = "rotated"
|
|
280
|
-
NOT_FOUND = "not_found"
|
|
281
|
-
REUSED = "reused"
|
|
282
|
-
EXPIRED = "expired"
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@dataclass(frozen=True, slots=True)
|
|
286
|
-
class RefreshRotationResult:
|
|
287
|
-
status: RefreshRotationStatus
|
|
288
|
-
session: RefreshSession | None
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
class IdentityRepository(Protocol):
|
|
292
|
-
async def find_user_by_normalized_username(
|
|
293
|
-
self, normalized_username: str
|
|
294
|
-
) -> IdentityUser | None: ...
|
|
295
|
-
|
|
296
|
-
async def create_user(self, user: IdentityUser) -> None: ...
|
|
297
|
-
|
|
298
|
-
async def get_user(self, user_id: UUID) -> IdentityUser: ...
|
|
299
|
-
|
|
300
|
-
async def get_user_view(self, user_id: UUID) -> UserView: ...
|
|
301
|
-
|
|
302
|
-
async def list_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
|
|
303
|
-
|
|
304
|
-
async def update_user(self, user_id: UUID, command: UpdateUserCommand) -> UserView: ...
|
|
305
|
-
|
|
306
|
-
async def set_user_active(self, user_id: UUID, command: SetUserActiveCommand) -> UserView: ...
|
|
307
|
-
|
|
308
|
-
async def delete_user(self, user_id: UUID, concurrency_version: int) -> None: ...
|
|
309
|
-
|
|
310
|
-
async def set_password(
|
|
311
|
-
self,
|
|
312
|
-
user_id: UUID,
|
|
313
|
-
password_hash: str,
|
|
314
|
-
concurrency_version: int,
|
|
315
|
-
*,
|
|
316
|
-
expected_password_hash: str | None = None,
|
|
317
|
-
) -> None: ...
|
|
318
|
-
|
|
319
|
-
async def create_role(self, role: IdentityRole) -> None: ...
|
|
320
|
-
|
|
321
|
-
async def find_role_by_normalized_name(self, normalized_name: str) -> IdentityRole | None: ...
|
|
322
|
-
|
|
323
|
-
async def get_role(self, role_id: UUID) -> RoleView: ...
|
|
324
|
-
|
|
325
|
-
async def list_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
|
|
326
|
-
|
|
327
|
-
async def update_role(self, role_id: UUID, command: UpdateRoleCommand) -> RoleView: ...
|
|
328
|
-
|
|
329
|
-
async def delete_role(self, role_id: UUID, concurrency_version: int) -> None: ...
|
|
330
|
-
|
|
331
|
-
async def set_user_roles(
|
|
332
|
-
self, user_id: UUID, role_ids: tuple[UUID, ...], concurrency_version: int
|
|
333
|
-
) -> UserRolesView: ...
|
|
334
|
-
|
|
335
|
-
async def set_role_permissions(
|
|
336
|
-
self, role_id: UUID, permissions: tuple[str, ...], concurrency_version: int
|
|
337
|
-
) -> RolePermissionsView: ...
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
class IdentityAccessStore(Protocol):
|
|
341
|
-
async def load_current_user(
|
|
342
|
-
self,
|
|
343
|
-
user_id: UUID,
|
|
344
|
-
session_id: UUID,
|
|
345
|
-
now: datetime,
|
|
346
|
-
) -> CurrentUser | None: ...
|
|
347
|
-
|
|
348
|
-
async def permissions_for_roles(
|
|
349
|
-
self, role_versions: tuple[RolePermissionVersion, ...]
|
|
350
|
-
) -> frozenset[str]: ...
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
class IdentityLoginStore(Protocol):
|
|
354
|
-
async def find_user(self, normalized_username: str) -> IdentityUser | None: ...
|
|
355
|
-
|
|
356
|
-
async def record_failure(
|
|
357
|
-
self,
|
|
358
|
-
user_id: UUID,
|
|
359
|
-
*,
|
|
360
|
-
now: datetime,
|
|
361
|
-
attempt_limit: int,
|
|
362
|
-
lockout_duration_seconds: int,
|
|
363
|
-
) -> None: ...
|
|
364
|
-
|
|
365
|
-
async def complete_success(
|
|
366
|
-
self,
|
|
367
|
-
user_id: UUID,
|
|
368
|
-
*,
|
|
369
|
-
expected_password_hash: str,
|
|
370
|
-
updated_password_hash: str | None,
|
|
371
|
-
session: RefreshSession,
|
|
372
|
-
now: datetime,
|
|
373
|
-
) -> bool: ...
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
class RefreshSessionStore(Protocol):
|
|
377
|
-
async def rotate(
|
|
378
|
-
self,
|
|
379
|
-
token_hash: str,
|
|
380
|
-
*,
|
|
381
|
-
replacement_id: UUID,
|
|
382
|
-
replacement_hash: str,
|
|
383
|
-
now: datetime,
|
|
384
|
-
) -> RefreshRotationResult: ...
|
|
385
|
-
|
|
386
|
-
async def revoke(self, token_hash: str, *, now: datetime) -> None: ...
|
|
15
|
+
IDENTITY_ROLES_MANAGE = PermissionName("identity.roles.manage")
|
|
387
16
|
|
|
388
17
|
|
|
389
18
|
class AuthenticationApplicationService(ApplicationServiceContract, Protocol):
|
|
390
|
-
async def login(self, command: LoginCommand) -> TokenPair: ...
|
|
19
|
+
async def login(self, command: dtos.LoginCommand) -> dtos.TokenPair: ...
|
|
391
20
|
|
|
392
|
-
async def refresh(self, command: RefreshTokenCommand) -> TokenPair: ...
|
|
21
|
+
async def refresh(self, command: dtos.RefreshTokenCommand) -> dtos.TokenPair: ...
|
|
393
22
|
|
|
394
|
-
async def revoke(self, command: RevokeTokenCommand) -> None: ...
|
|
23
|
+
async def revoke(self, command: dtos.RevokeTokenCommand) -> None: ...
|
|
395
24
|
|
|
396
|
-
async def me(self) -> CurrentUserView: ...
|
|
25
|
+
async def me(self) -> dtos.CurrentUserView: ...
|
|
397
26
|
|
|
398
|
-
async def change_password(self, command: ChangePasswordCommand) -> None: ...
|
|
27
|
+
async def change_password(self, command: dtos.ChangePasswordCommand) -> None: ...
|
|
399
28
|
|
|
400
29
|
|
|
401
30
|
class IdentityManagementApplicationService(ApplicationServiceContract, Protocol):
|
|
402
|
-
async def create_user(self, command: CreateUserCommand) -> UserView: ...
|
|
31
|
+
async def create_user(self, command: dtos.CreateUserCommand) -> dtos.UserView: ...
|
|
403
32
|
|
|
404
|
-
async def get_user(self, user_id: UUID) -> UserView: ...
|
|
33
|
+
async def get_user(self, user_id: UUID) -> dtos.UserView: ...
|
|
405
34
|
|
|
406
|
-
async def get_users(self, query: PagedResultRequestDto) -> PagedResult[UserView]: ...
|
|
35
|
+
async def get_users(self, query: PagedResultRequestDto) -> PagedResult[dtos.UserView]: ...
|
|
407
36
|
|
|
408
|
-
async def update_user(
|
|
37
|
+
async def update_user(
|
|
38
|
+
self, user_id: UUID, command: dtos.UpdateUserCommand
|
|
39
|
+
) -> dtos.UserView: ...
|
|
409
40
|
|
|
410
|
-
async def set_user_active(
|
|
41
|
+
async def set_user_active(
|
|
42
|
+
self, user_id: UUID, command: dtos.SetUserActiveCommand
|
|
43
|
+
) -> dtos.UserView: ...
|
|
411
44
|
|
|
412
|
-
async def delete_user(
|
|
45
|
+
async def delete_user(
|
|
46
|
+
self, user_id: UUID, command: dtos.IdentityConcurrencyCommand
|
|
47
|
+
) -> None: ...
|
|
413
48
|
|
|
414
|
-
async def set_password(self, user_id: UUID, command: SetPasswordCommand) -> None: ...
|
|
49
|
+
async def set_password(self, user_id: UUID, command: dtos.SetPasswordCommand) -> None: ...
|
|
415
50
|
|
|
416
|
-
async def create_role(self, command: CreateRoleCommand) -> RoleView: ...
|
|
51
|
+
async def create_role(self, command: dtos.CreateRoleCommand) -> dtos.RoleView: ...
|
|
417
52
|
|
|
418
|
-
async def get_role(self, role_id: UUID) -> RoleView: ...
|
|
53
|
+
async def get_role(self, role_id: UUID) -> dtos.RoleView: ...
|
|
419
54
|
|
|
420
|
-
async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[RoleView]: ...
|
|
55
|
+
async def get_roles(self, query: PagedResultRequestDto) -> PagedResult[dtos.RoleView]: ...
|
|
421
56
|
|
|
422
|
-
async def update_role(
|
|
57
|
+
async def update_role(
|
|
58
|
+
self, role_id: UUID, command: dtos.UpdateRoleCommand
|
|
59
|
+
) -> dtos.RoleView: ...
|
|
423
60
|
|
|
424
|
-
async def delete_role(
|
|
61
|
+
async def delete_role(
|
|
62
|
+
self, role_id: UUID, command: dtos.IdentityConcurrencyCommand
|
|
63
|
+
) -> None: ...
|
|
425
64
|
|
|
426
|
-
async def get_user_roles(self, user_id: UUID) -> UserRolesView: ...
|
|
65
|
+
async def get_user_roles(self, user_id: UUID) -> dtos.UserRolesView: ...
|
|
427
66
|
|
|
428
|
-
async def get_role_permissions(self, role_id: UUID) -> RolePermissionsView: ...
|
|
67
|
+
async def get_role_permissions(self, role_id: UUID) -> dtos.RolePermissionsView: ...
|
|
429
68
|
|
|
430
69
|
async def set_user_roles(
|
|
431
|
-
self, user_id: UUID, command: SetUserRolesCommand
|
|
432
|
-
) -> UserRolesView: ...
|
|
70
|
+
self, user_id: UUID, command: dtos.SetUserRolesCommand
|
|
71
|
+
) -> dtos.UserRolesView: ...
|
|
433
72
|
|
|
434
73
|
async def set_role_permissions(
|
|
435
|
-
self, role_id: UUID, command: SetRolePermissionsCommand
|
|
436
|
-
) -> RolePermissionsView: ...
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
def _normalize_name(value: str, *, role: str) -> str:
|
|
440
|
-
normalized = value.strip().casefold()
|
|
441
|
-
if not normalized or len(normalized) > 120:
|
|
442
|
-
raise ValueError(f"{role} must contain between 1 and 120 characters")
|
|
443
|
-
return normalized
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
def _check_version(actual: int, expected: int) -> None:
|
|
447
|
-
if actual != expected:
|
|
448
|
-
raise OptimisticConcurrencyError
|
|
74
|
+
self, role_id: UUID, command: dtos.SetRolePermissionsCommand
|
|
75
|
+
) -> dtos.RolePermissionsView: ...
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Identity 名称规范化与领域并发版本规则。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..domain import OptimisticConcurrencyError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _normalize_name(value: str, *, role: str) -> str:
|
|
9
|
+
normalized = value.strip().casefold()
|
|
10
|
+
if not normalized or len(normalized) > 120:
|
|
11
|
+
raise ValueError(f"{role} must contain between 1 and 120 characters")
|
|
12
|
+
return normalized
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _check_version(actual: int, expected: int) -> None:
|
|
16
|
+
if actual != expected:
|
|
17
|
+
raise OptimisticConcurrencyError
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Identity 应用服务的输入、输出与并发版本。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, field_validator
|
|
10
|
+
|
|
11
|
+
from ..extensions.contracts import ExtensibleModel
|
|
12
|
+
from .domain_rules import _normalize_name
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LoginCommand(BaseModel):
|
|
16
|
+
model_config = ConfigDict(frozen=True)
|
|
17
|
+
|
|
18
|
+
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
19
|
+
password: SecretStr
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RefreshTokenCommand(BaseModel):
|
|
23
|
+
model_config = ConfigDict(frozen=True)
|
|
24
|
+
|
|
25
|
+
refresh_token: SecretStr
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RevokeTokenCommand(BaseModel):
|
|
29
|
+
model_config = ConfigDict(frozen=True)
|
|
30
|
+
|
|
31
|
+
refresh_token: SecretStr
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CreateUserCommand(ExtensibleModel):
|
|
35
|
+
model_config = ConfigDict(frozen=True)
|
|
36
|
+
|
|
37
|
+
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
38
|
+
password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
39
|
+
|
|
40
|
+
@field_validator("username")
|
|
41
|
+
@classmethod
|
|
42
|
+
def _validate_username(cls, value: str) -> str:
|
|
43
|
+
_normalize_name(value, role="username")
|
|
44
|
+
return value.strip()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CreateRoleCommand(BaseModel):
|
|
48
|
+
model_config = ConfigDict(frozen=True)
|
|
49
|
+
|
|
50
|
+
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
51
|
+
|
|
52
|
+
@field_validator("name")
|
|
53
|
+
@classmethod
|
|
54
|
+
def _validate_name(cls, value: str) -> str:
|
|
55
|
+
_normalize_name(value, role="role name")
|
|
56
|
+
return value.strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IdentityConcurrencyCommand(BaseModel):
|
|
60
|
+
model_config = ConfigDict(frozen=True)
|
|
61
|
+
|
|
62
|
+
concurrency_version: Annotated[int, Field(ge=0)]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UpdateUserCommand(ExtensibleModel):
|
|
66
|
+
model_config = ConfigDict(frozen=True)
|
|
67
|
+
|
|
68
|
+
username: Annotated[str, Field(min_length=1, max_length=120)]
|
|
69
|
+
concurrency_version: Annotated[int, Field(ge=0)]
|
|
70
|
+
|
|
71
|
+
@field_validator("username")
|
|
72
|
+
@classmethod
|
|
73
|
+
def _validate_username(cls, value: str) -> str:
|
|
74
|
+
_normalize_name(value, role="username")
|
|
75
|
+
return value.strip()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SetUserActiveCommand(IdentityConcurrencyCommand):
|
|
79
|
+
is_active: bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class UpdateRoleCommand(IdentityConcurrencyCommand):
|
|
83
|
+
name: Annotated[str, Field(min_length=1, max_length=120)]
|
|
84
|
+
|
|
85
|
+
@field_validator("name")
|
|
86
|
+
@classmethod
|
|
87
|
+
def _validate_name(cls, value: str) -> str:
|
|
88
|
+
_normalize_name(value, role="role name")
|
|
89
|
+
return value.strip()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ChangePasswordCommand(IdentityConcurrencyCommand):
|
|
93
|
+
old_password: SecretStr
|
|
94
|
+
new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SetPasswordCommand(IdentityConcurrencyCommand):
|
|
98
|
+
new_password: Annotated[SecretStr, Field(min_length=12, max_length=1024)]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SetUserRolesCommand(IdentityConcurrencyCommand):
|
|
102
|
+
model_config = ConfigDict(frozen=True)
|
|
103
|
+
|
|
104
|
+
role_ids: tuple[UUID, ...]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class SetRolePermissionsCommand(IdentityConcurrencyCommand):
|
|
108
|
+
model_config = ConfigDict(frozen=True)
|
|
109
|
+
|
|
110
|
+
permissions: tuple[str, ...]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class TokenPair:
|
|
115
|
+
access_token: str
|
|
116
|
+
refresh_token: str
|
|
117
|
+
token_type: str
|
|
118
|
+
expires_in: int
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class UserView(ExtensibleModel):
|
|
122
|
+
user_id: UUID
|
|
123
|
+
username: str
|
|
124
|
+
is_active: bool
|
|
125
|
+
role_ids: tuple[UUID, ...]
|
|
126
|
+
concurrency_version: int
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True, slots=True)
|
|
130
|
+
class RoleView:
|
|
131
|
+
role_id: UUID
|
|
132
|
+
name: str
|
|
133
|
+
permissions: tuple[str, ...]
|
|
134
|
+
concurrency_version: int
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class UserRolesView(BaseModel):
|
|
138
|
+
role_ids: tuple[UUID, ...]
|
|
139
|
+
concurrency_version: int
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class RolePermissionsView(BaseModel):
|
|
143
|
+
permissions: tuple[str, ...]
|
|
144
|
+
concurrency_version: int
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass(frozen=True, slots=True)
|
|
148
|
+
class CurrentUserView:
|
|
149
|
+
user_id: UUID
|
|
150
|
+
session_id: UUID
|
|
151
|
+
username: str
|
|
152
|
+
concurrency_version: int
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Identity 用户扩展点连接 DTO 与领域值,由所属 Module 声明。"""
|
|
2
|
+
|
|
3
|
+
from ..extensions.contracts import ExtensionTarget
|
|
4
|
+
from .dtos import CreateUserCommand, UpdateUserCommand, UserView
|
|
5
|
+
from .models import IdentityUser
|
|
6
|
+
|
|
7
|
+
IDENTITY_USER_EXTENSION = ExtensionTarget(
|
|
8
|
+
"identity.user", (CreateUserCommand, UpdateUserCommand, UserView), IdentityUser
|
|
9
|
+
)
|