messagefoundry 0.1.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.
- messagefoundry/__init__.py +108 -0
- messagefoundry/__main__.py +1155 -0
- messagefoundry/api/__init__.py +27 -0
- messagefoundry/api/app.py +1581 -0
- messagefoundry/api/approvals.py +184 -0
- messagefoundry/api/auth_models.py +211 -0
- messagefoundry/api/auth_routes.py +655 -0
- messagefoundry/api/field_authz.py +96 -0
- messagefoundry/api/models.py +374 -0
- messagefoundry/api/security.py +247 -0
- messagefoundry/api/tls.py +47 -0
- messagefoundry/auth/__init__.py +39 -0
- messagefoundry/auth/data/common_passwords.NOTICE +13 -0
- messagefoundry/auth/data/common_passwords.txt +10000 -0
- messagefoundry/auth/identity.py +71 -0
- messagefoundry/auth/ldap.py +264 -0
- messagefoundry/auth/notifications.py +68 -0
- messagefoundry/auth/passwords.py +53 -0
- messagefoundry/auth/permissions.py +120 -0
- messagefoundry/auth/policy.py +153 -0
- messagefoundry/auth/ratelimit.py +55 -0
- messagefoundry/auth/service.py +1323 -0
- messagefoundry/auth/tokens.py +26 -0
- messagefoundry/auth/totp.py +174 -0
- messagefoundry/checks.py +174 -0
- messagefoundry/config/__init__.py +30 -0
- messagefoundry/config/active_environment.py +80 -0
- messagefoundry/config/ai_policy.py +140 -0
- messagefoundry/config/code_sets.py +260 -0
- messagefoundry/config/connections_edit.py +200 -0
- messagefoundry/config/connections_file.py +287 -0
- messagefoundry/config/db_lookup.py +117 -0
- messagefoundry/config/environments.py +116 -0
- messagefoundry/config/ingest_time.py +83 -0
- messagefoundry/config/models.py +240 -0
- messagefoundry/config/reference.py +158 -0
- messagefoundry/config/response.py +83 -0
- messagefoundry/config/run_context.py +153 -0
- messagefoundry/config/settings.py +1311 -0
- messagefoundry/config/state.py +99 -0
- messagefoundry/config/tls_policy.py +110 -0
- messagefoundry/config/wiring.py +1918 -0
- messagefoundry/console/__init__.py +20 -0
- messagefoundry/console/__main__.py +274 -0
- messagefoundry/console/_async.py +107 -0
- messagefoundry/console/change_password.py +111 -0
- messagefoundry/console/client.py +552 -0
- messagefoundry/console/connections.py +324 -0
- messagefoundry/console/login.py +107 -0
- messagefoundry/console/mfa.py +205 -0
- messagefoundry/console/reauth.py +94 -0
- messagefoundry/console/search.py +57 -0
- messagefoundry/console/service_control.py +137 -0
- messagefoundry/console/sessions.py +122 -0
- messagefoundry/console/shell.py +410 -0
- messagefoundry/console/status.py +377 -0
- messagefoundry/console/users_page.py +282 -0
- messagefoundry/console/widgets.py +553 -0
- messagefoundry/generators/README.md +27 -0
- messagefoundry/generators/__init__.py +15 -0
- messagefoundry/generators/_core.py +589 -0
- messagefoundry/generators/_hl7data.py +428 -0
- messagefoundry/generators/adt.py +286 -0
- messagefoundry/generators/all_types.py +24 -0
- messagefoundry/generators/bar.py +28 -0
- messagefoundry/generators/dft.py +20 -0
- messagefoundry/generators/mdm.py +39 -0
- messagefoundry/generators/mfn.py +46 -0
- messagefoundry/generators/oml.py +32 -0
- messagefoundry/generators/orl.py +30 -0
- messagefoundry/generators/orm.py +23 -0
- messagefoundry/generators/oru.py +21 -0
- messagefoundry/generators/ras.py +20 -0
- messagefoundry/generators/rde.py +54 -0
- messagefoundry/generators/siu.py +64 -0
- messagefoundry/generators/vxu.py +20 -0
- messagefoundry/hl7schema.py +75 -0
- messagefoundry/last_resort.py +55 -0
- messagefoundry/logging_setup.py +332 -0
- messagefoundry/parsing/__init__.py +64 -0
- messagefoundry/parsing/consistency.py +166 -0
- messagefoundry/parsing/groups.py +228 -0
- messagefoundry/parsing/message.py +453 -0
- messagefoundry/parsing/peek.py +237 -0
- messagefoundry/parsing/split.py +120 -0
- messagefoundry/parsing/summary.py +46 -0
- messagefoundry/parsing/tree.py +128 -0
- messagefoundry/parsing/validate.py +95 -0
- messagefoundry/parsing/x12/__init__.py +46 -0
- messagefoundry/parsing/x12/delimiters.py +140 -0
- messagefoundry/parsing/x12/errors.py +30 -0
- messagefoundry/parsing/x12/interchange.py +232 -0
- messagefoundry/parsing/x12/message.py +200 -0
- messagefoundry/parsing/x12/peek.py +207 -0
- messagefoundry/pipeline/__init__.py +21 -0
- messagefoundry/pipeline/alert_sinks.py +486 -0
- messagefoundry/pipeline/alerts.py +100 -0
- messagefoundry/pipeline/cert_expiry.py +219 -0
- messagefoundry/pipeline/cluster.py +955 -0
- messagefoundry/pipeline/cluster_sqlserver.py +444 -0
- messagefoundry/pipeline/config_convergence.py +137 -0
- messagefoundry/pipeline/dryrun.py +450 -0
- messagefoundry/pipeline/engine.py +756 -0
- messagefoundry/pipeline/leader_tasks.py +158 -0
- messagefoundry/pipeline/reference_sync.py +369 -0
- messagefoundry/pipeline/retention.py +289 -0
- messagefoundry/pipeline/security_notify.py +168 -0
- messagefoundry/pipeline/state_convergence.py +143 -0
- messagefoundry/pipeline/wiring_runner.py +1722 -0
- messagefoundry/py.typed +0 -0
- messagefoundry/redaction.py +71 -0
- messagefoundry/scaffold.py +321 -0
- messagefoundry/secrets_dpapi.py +129 -0
- messagefoundry/store/__init__.py +46 -0
- messagefoundry/store/audit_tee.py +67 -0
- messagefoundry/store/base.py +758 -0
- messagefoundry/store/crypto.py +166 -0
- messagefoundry/store/keyprovider.py +192 -0
- messagefoundry/store/postgres.py +3447 -0
- messagefoundry/store/sqlserver.py +3014 -0
- messagefoundry/store/store.py +3790 -0
- messagefoundry/timezone.py +207 -0
- messagefoundry/transports/__init__.py +50 -0
- messagefoundry/transports/base.py +269 -0
- messagefoundry/transports/database.py +693 -0
- messagefoundry/transports/file.py +551 -0
- messagefoundry/transports/framing.py +164 -0
- messagefoundry/transports/loopback.py +53 -0
- messagefoundry/transports/mllp.py +644 -0
- messagefoundry/transports/remotefile.py +664 -0
- messagefoundry/transports/rest.py +281 -0
- messagefoundry/transports/signing.py +321 -0
- messagefoundry/transports/soap.py +507 -0
- messagefoundry/transports/tcp.py +307 -0
- messagefoundry/transports/timer.py +146 -0
- messagefoundry/transports/x12.py +323 -0
- messagefoundry-0.1.0.dist-info/METADATA +212 -0
- messagefoundry-0.1.0.dist-info/RECORD +142 -0
- messagefoundry-0.1.0.dist-info/WHEEL +4 -0
- messagefoundry-0.1.0.dist-info/entry_points.txt +2 -0
- messagefoundry-0.1.0.dist-info/licenses/LICENSE +662 -0
- messagefoundry-0.1.0.dist-info/licenses/NOTICE +27 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Dual-control (maker-checker) approval workflow for high-value actions (ASVS 2.3.5).
|
|
4
|
+
|
|
5
|
+
Optional and **deny-by-default** (``[approvals]``, off unless enabled). When a gated operation is
|
|
6
|
+
invoked it is **not executed inline**: a pending request (operation key + JSON params + requester) is
|
|
7
|
+
persisted, and a **distinct** second user holding ``approvals:approve`` must release it — the requester
|
|
8
|
+
can never approve their own (enforced server-side). On approval the captured operation is re-executed
|
|
9
|
+
and **both identities** land in the hash-chained audit log. A request older than
|
|
10
|
+
``[approvals].expiry_hours`` can no longer be approved.
|
|
11
|
+
|
|
12
|
+
The registry (op key -> executor) is populated by the API wiring, where the engine is in scope; this
|
|
13
|
+
module owns only the generic hold/approve/reject mechanics over the ``pending_approvals`` store table.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import Any
|
|
23
|
+
from uuid import uuid4
|
|
24
|
+
|
|
25
|
+
from messagefoundry.config.settings import ApprovalsSettings
|
|
26
|
+
from messagefoundry.store.base import Store
|
|
27
|
+
|
|
28
|
+
#: An executor re-runs a captured operation on approval, returning a small JSON-able result summary.
|
|
29
|
+
Executor = Callable[[Mapping[str, Any]], Awaitable[dict[str, Any]]]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class _Operation:
|
|
34
|
+
key: str
|
|
35
|
+
label: str # human description, surfaced in the pending list + audit
|
|
36
|
+
execute: Executor
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ApprovalError(Exception):
|
|
40
|
+
"""A pending-approval decision could not be made. ``status`` is the HTTP code the API should map
|
|
41
|
+
to (404 unknown, 409 already-decided/expired, 403 self-approval)."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, status: int, detail: str) -> None:
|
|
44
|
+
super().__init__(detail)
|
|
45
|
+
self.status = status
|
|
46
|
+
self.detail = detail
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ApprovalGate:
|
|
50
|
+
"""Holds the registry of approvable operations and the hold/approve/reject mechanics. One instance
|
|
51
|
+
per app; created with the live store + the resolved ``[approvals]`` settings."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, store: Store, settings: ApprovalsSettings) -> None:
|
|
54
|
+
self._store = store
|
|
55
|
+
self._settings = settings
|
|
56
|
+
self._ops: dict[str, _Operation] = {}
|
|
57
|
+
|
|
58
|
+
def register(self, key: str, label: str, execute: Executor) -> None:
|
|
59
|
+
self._ops[key] = _Operation(key=key, label=label, execute=execute)
|
|
60
|
+
|
|
61
|
+
def _gated(self, operation: str) -> bool:
|
|
62
|
+
return self._settings.enabled and operation in self._settings.operations
|
|
63
|
+
|
|
64
|
+
async def guard(
|
|
65
|
+
self, operation: str, params: Mapping[str, Any], *, requester: str
|
|
66
|
+
) -> str | None:
|
|
67
|
+
"""Call at the start of a gated endpoint, **after** the requester's own permission/scope checks
|
|
68
|
+
pass. If dual-control is active for ``operation``, persist a pending request, audit
|
|
69
|
+
``approval.requested``, and return its **id** (the endpoint should respond 202). Otherwise
|
|
70
|
+
return ``None`` — the endpoint executes inline exactly as before."""
|
|
71
|
+
if not self._gated(operation):
|
|
72
|
+
return None
|
|
73
|
+
now = time.time()
|
|
74
|
+
approval_id = uuid4().hex
|
|
75
|
+
expires_at = (
|
|
76
|
+
None if self._settings.expiry_hours == 0 else now + self._settings.expiry_hours * 3600.0
|
|
77
|
+
)
|
|
78
|
+
await self._store.create_pending_approval(
|
|
79
|
+
approval_id=approval_id,
|
|
80
|
+
operation=operation,
|
|
81
|
+
params=json.dumps(dict(params), sort_keys=True),
|
|
82
|
+
requester=requester,
|
|
83
|
+
requested_at=now,
|
|
84
|
+
expires_at=expires_at,
|
|
85
|
+
)
|
|
86
|
+
await self._store.record_audit(
|
|
87
|
+
"approval.requested",
|
|
88
|
+
actor=requester,
|
|
89
|
+
detail=json.dumps({"approval_id": approval_id, "operation": operation}),
|
|
90
|
+
)
|
|
91
|
+
return approval_id
|
|
92
|
+
|
|
93
|
+
async def list_pending(self) -> list[dict[str, Any]]:
|
|
94
|
+
rows = await self._store.list_pending_approvals(now=time.time())
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
"id": str(r["id"]),
|
|
98
|
+
"operation": str(r["operation"]),
|
|
99
|
+
"label": self._label(str(r["operation"])),
|
|
100
|
+
"requester": str(r["requester"]),
|
|
101
|
+
"requested_at": float(r["requested_at"]),
|
|
102
|
+
"expires_at": (None if r["expires_at"] is None else float(r["expires_at"])),
|
|
103
|
+
}
|
|
104
|
+
for r in rows
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
async def approve(self, approval_id: str, *, approver: str) -> dict[str, Any]:
|
|
108
|
+
"""Release a pending request: the captured operation is re-executed and both identities are
|
|
109
|
+
audited. Refuses self-approval (the requester is not a valid second approver)."""
|
|
110
|
+
row = await self._require_pending(approval_id)
|
|
111
|
+
if str(row["requester"]) == approver:
|
|
112
|
+
raise ApprovalError(403, "you cannot approve your own request")
|
|
113
|
+
operation = str(row["operation"])
|
|
114
|
+
op = self._ops.get(operation)
|
|
115
|
+
if (
|
|
116
|
+
op is None
|
|
117
|
+
): # registered op was removed between request and approval — refuse, stay pending
|
|
118
|
+
raise ApprovalError(409, f"operation '{operation}' is no longer available")
|
|
119
|
+
# Transition to 'approved' FIRST (atomic, guards a double-approve race); only then execute.
|
|
120
|
+
if not await self._store.decide_pending_approval(
|
|
121
|
+
approval_id, status="approved", approver=approver, decided_at=time.time()
|
|
122
|
+
):
|
|
123
|
+
raise ApprovalError(409, "request was already decided")
|
|
124
|
+
params = json.loads(str(row["params"]))
|
|
125
|
+
result = await op.execute(params)
|
|
126
|
+
await self._store.record_audit(
|
|
127
|
+
"approval.approved",
|
|
128
|
+
actor=approver,
|
|
129
|
+
detail=json.dumps(
|
|
130
|
+
{
|
|
131
|
+
"approval_id": approval_id,
|
|
132
|
+
"operation": operation,
|
|
133
|
+
"requester": str(row["requester"]),
|
|
134
|
+
"result": result,
|
|
135
|
+
}
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
return {
|
|
139
|
+
"operation": operation,
|
|
140
|
+
"requested_by": str(row["requester"]),
|
|
141
|
+
"approved_by": approver,
|
|
142
|
+
"result": result,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async def reject(self, approval_id: str, *, approver: str) -> dict[str, Any]:
|
|
146
|
+
"""Decline a pending request without executing it (audited). Any ``approvals:approve`` holder
|
|
147
|
+
may reject — including the requester cancelling their own."""
|
|
148
|
+
row = await self._require_pending(approval_id)
|
|
149
|
+
if not await self._store.decide_pending_approval(
|
|
150
|
+
approval_id, status="rejected", approver=approver, decided_at=time.time()
|
|
151
|
+
):
|
|
152
|
+
raise ApprovalError(409, "request was already decided")
|
|
153
|
+
operation = str(row["operation"])
|
|
154
|
+
await self._store.record_audit(
|
|
155
|
+
"approval.rejected",
|
|
156
|
+
actor=approver,
|
|
157
|
+
detail=json.dumps(
|
|
158
|
+
{
|
|
159
|
+
"approval_id": approval_id,
|
|
160
|
+
"operation": operation,
|
|
161
|
+
"requester": str(row["requester"]),
|
|
162
|
+
}
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
return {
|
|
166
|
+
"operation": operation,
|
|
167
|
+
"requested_by": str(row["requester"]),
|
|
168
|
+
"rejected_by": approver,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async def _require_pending(self, approval_id: str) -> Any:
|
|
172
|
+
row = await self._store.get_pending_approval(approval_id)
|
|
173
|
+
if row is None:
|
|
174
|
+
raise ApprovalError(404, "no such approval request")
|
|
175
|
+
if str(row["status"]) != "pending":
|
|
176
|
+
raise ApprovalError(409, f"request is already {row['status']}")
|
|
177
|
+
expires_at = row["expires_at"]
|
|
178
|
+
if expires_at is not None and float(expires_at) <= time.time():
|
|
179
|
+
raise ApprovalError(409, "request has expired")
|
|
180
|
+
return row
|
|
181
|
+
|
|
182
|
+
def _label(self, operation: str) -> str:
|
|
183
|
+
op = self._ops.get(operation)
|
|
184
|
+
return op.label if op is not None else operation
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Pydantic request/response models for the auth + user-administration endpoints."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
# Upper bounds on free-text request fields (API-INPUT): reject absurd inputs before they reach the
|
|
10
|
+
# store or argon2. Generous vs any legitimate value; the password cap also bounds argon2 work.
|
|
11
|
+
_NAME_MAX = 256
|
|
12
|
+
_PASSWORD_MAX = 1024
|
|
13
|
+
_GROUP_MAX = 512
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoginRequest(BaseModel):
|
|
17
|
+
username: str = Field(max_length=_NAME_MAX)
|
|
18
|
+
password: str = Field(max_length=_PASSWORD_MAX)
|
|
19
|
+
provider: str = Field("local", max_length=16) # 'local' | 'ad'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CurrentUser(BaseModel):
|
|
23
|
+
user_id: str
|
|
24
|
+
username: str
|
|
25
|
+
auth_provider: str
|
|
26
|
+
roles: list[str]
|
|
27
|
+
permissions: list[str]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LoginResponse(BaseModel):
|
|
31
|
+
token: str
|
|
32
|
+
token_type: str = "bearer"
|
|
33
|
+
must_change_password: bool = False
|
|
34
|
+
# The password was accepted but a second factor is still required before sensitive operations
|
|
35
|
+
# (WP-14): the client should prompt for a TOTP / recovery code and POST /auth/mfa-verify.
|
|
36
|
+
mfa_required: bool = False
|
|
37
|
+
user: CurrentUser
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProvidersInfo(BaseModel):
|
|
41
|
+
"""What the login screen should offer."""
|
|
42
|
+
|
|
43
|
+
local: bool = True
|
|
44
|
+
ad: bool = False
|
|
45
|
+
kerberos: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserSummary(BaseModel):
|
|
49
|
+
id: str
|
|
50
|
+
username: str
|
|
51
|
+
auth_provider: str
|
|
52
|
+
display_name: str | None = None
|
|
53
|
+
email: str | None = None
|
|
54
|
+
disabled: bool
|
|
55
|
+
roles: list[str]
|
|
56
|
+
channel_scope: list[str] | None = None # per-channel RBAC: allowed connections; None = all
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ChannelScope(BaseModel):
|
|
60
|
+
"""A user's per-channel RBAC scope. ``None`` = all channels; a list = exactly those connections."""
|
|
61
|
+
|
|
62
|
+
channels: list[str] | None = Field(default=None, max_length=512)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UserCreateRequest(BaseModel):
|
|
66
|
+
username: str = Field(max_length=_NAME_MAX)
|
|
67
|
+
password: str = Field(max_length=_PASSWORD_MAX)
|
|
68
|
+
display_name: str | None = Field(default=None, max_length=_NAME_MAX)
|
|
69
|
+
email: str | None = Field(default=None, max_length=_NAME_MAX)
|
|
70
|
+
roles: list[str] = Field(default=[], max_length=64)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class UserUpdateRequest(BaseModel):
|
|
74
|
+
display_name: str | None = Field(default=None, max_length=_NAME_MAX)
|
|
75
|
+
email: str | None = Field(default=None, max_length=_NAME_MAX)
|
|
76
|
+
disabled: bool | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RolesUpdateRequest(BaseModel):
|
|
80
|
+
roles: list[str] = Field(max_length=64)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class PasswordChangeRequest(BaseModel):
|
|
84
|
+
current_password: str = Field(max_length=_PASSWORD_MAX)
|
|
85
|
+
new_password: str = Field(max_length=_PASSWORD_MAX)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ReauthRequest(BaseModel):
|
|
89
|
+
"""Step-up re-verification (ASVS 7.5.3): the caller re-supplies their current credential to refresh
|
|
90
|
+
the session's step-up window before a highly sensitive operation."""
|
|
91
|
+
|
|
92
|
+
password: str = Field(max_length=_PASSWORD_MAX)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PasswordResetResponse(BaseModel):
|
|
96
|
+
"""The result of an admin password reset (ASVS 6.4.6): a one-time credential returned **once** for
|
|
97
|
+
the administrator to convey out-of-band. The user must change it on first login."""
|
|
98
|
+
|
|
99
|
+
temp_password: str
|
|
100
|
+
must_change_password: bool = True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --- MFA: native TOTP second factor (WP-14, ASVS 6.3.3) ----------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class MfaVerifyRequest(BaseModel):
|
|
107
|
+
"""Satisfy a session's second factor with a TOTP code **or** a single-use recovery code."""
|
|
108
|
+
|
|
109
|
+
code: str = Field(max_length=64)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MfaEnrollResponse(BaseModel):
|
|
113
|
+
"""A staged (not-yet-active) TOTP enrollment: the base32 secret + the ``otpauth://`` URI the
|
|
114
|
+
console renders as a QR. Returned once; the secret is not active until confirmed."""
|
|
115
|
+
|
|
116
|
+
secret: str
|
|
117
|
+
otpauth_uri: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class MfaConfirmRequest(BaseModel):
|
|
121
|
+
"""Confirm a staged enrollment by proving a live TOTP code from the authenticator app."""
|
|
122
|
+
|
|
123
|
+
code: str = Field(max_length=16)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class MfaConfirmResponse(BaseModel):
|
|
127
|
+
"""The one-time single-use recovery codes minted on enrollment — shown **once** for the user to
|
|
128
|
+
save (lost-authenticator escape hatch)."""
|
|
129
|
+
|
|
130
|
+
recovery_codes: list[str]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class MfaStatusResponse(BaseModel):
|
|
134
|
+
"""The caller's current MFA posture for ``GET /me/mfa``."""
|
|
135
|
+
|
|
136
|
+
enabled: bool
|
|
137
|
+
enrolled_at: float | None = None
|
|
138
|
+
recovery_codes_remaining: int = 0
|
|
139
|
+
required: bool = False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class RoleInfo(BaseModel):
|
|
143
|
+
id: str
|
|
144
|
+
display_name: str
|
|
145
|
+
description: str | None = None
|
|
146
|
+
permissions: list[str]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class AdGroupMapEntry(BaseModel):
|
|
150
|
+
ad_group: str = Field(max_length=_GROUP_MAX)
|
|
151
|
+
role: str = Field(max_length=64)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class AdGroupMap(BaseModel):
|
|
155
|
+
entries: list[AdGroupMapEntry]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class AdGroupScopeEntry(BaseModel):
|
|
159
|
+
"""Maps an AD group to one allowed channel; channel ``*`` = all channels (per-channel RBAC C3)."""
|
|
160
|
+
|
|
161
|
+
ad_group: str = Field(max_length=_GROUP_MAX)
|
|
162
|
+
channel: str = Field(max_length=_NAME_MAX)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AdGroupScopeMap(BaseModel):
|
|
166
|
+
entries: list[AdGroupScopeEntry]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class AuditEntry(BaseModel):
|
|
170
|
+
ts: float
|
|
171
|
+
actor: str | None = None
|
|
172
|
+
action: str
|
|
173
|
+
channel_id: str | None = None
|
|
174
|
+
detail: str | None = None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AuditList(BaseModel):
|
|
178
|
+
entries: list[AuditEntry]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SimpleMessage(BaseModel):
|
|
182
|
+
detail: str
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class SessionInfo(BaseModel):
|
|
186
|
+
"""One active session in the self-service inventory (WP-10). ``id`` is the session's ``token_hash``
|
|
187
|
+
(a one-way hash of the opaque token, safe to expose) — pass it to ``DELETE /me/sessions/{id}``."""
|
|
188
|
+
|
|
189
|
+
id: str
|
|
190
|
+
created_at: float
|
|
191
|
+
last_used_at: float
|
|
192
|
+
expires_at: float
|
|
193
|
+
client: str | None = None
|
|
194
|
+
current: bool = False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class SessionList(BaseModel):
|
|
198
|
+
sessions: list[SessionInfo]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class SecurityEventInfo(BaseModel):
|
|
202
|
+
"""One entry in the caller's security-event history (WP-L3-05, ASVS 6.3.5/6.3.7) — a view over the
|
|
203
|
+
audited ``auth.*`` actions. ``detail`` is the audit log's JSON metadata (PHI-free)."""
|
|
204
|
+
|
|
205
|
+
ts: float
|
|
206
|
+
action: str # e.g. auth.login_success / auth.login_locked / auth.password_changed
|
|
207
|
+
detail: str | None = None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class SecurityEventsList(BaseModel):
|
|
211
|
+
events: list[SecurityEventInfo]
|