feat(cloud-console): add user authentication and administration

This commit is contained in:
2026-07-13 17:54:53 +08:00
parent 035b177128
commit cdef630e67
35 changed files with 4126 additions and 113 deletions
+101 -1
View File
@@ -1,13 +1,20 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, Literal, Protocol
if TYPE_CHECKING:
from cloud.plugins import PluginManifest
from cloud.pool import HostRegistration, PooledDevice
from cloud.scheduler import ScheduledTask, ScheduledTaskStatus
from cloud.user_auth import (
AuthAuditEvent,
AuthenticatedUserSession,
LoginThrottle,
UserAccount,
UserSession,
)
AttemptStatus = Literal["assigned", "dispatched", "done", "failed", "expired"]
@@ -28,6 +35,14 @@ class DeviceEnrollmentConflictError(RuntimeError):
"""Raised when a local device enrollment conflicts with stored identity."""
class UserConflictError(RuntimeError):
"""Raised when a user operation violates a durable identity invariant."""
class LastAdministratorConflictError(UserConflictError):
"""Raised when a write would remove the last enabled administrator."""
@dataclass(frozen=True)
class HostEnrollment:
host_id: str
@@ -183,6 +198,91 @@ class CloudRepository(Protocol):
def get_plugin(self, name: str) -> tuple[PluginManifest, bool] | None: ...
def create_user(self, user: UserAccount) -> UserAccount: ...
def get_user(self, user_id: str) -> UserAccount | None: ...
def get_user_by_normalized_username(
self,
username_normalized: str,
) -> UserAccount | None: ...
def list_users(self, *, limit: int, offset: int) -> list[UserAccount]: ...
def update_user(
self,
user_id: str,
*,
display_name: str | None = None,
role: str | None = None,
enabled: bool | None = None,
updated_at: datetime,
) -> UserAccount: ...
def rehash_user_password(
self,
user_id: str,
*,
password_hash: str,
updated_at: datetime,
) -> UserAccount: ...
def update_user_password(
self,
user_id: str,
*,
password_hash: str,
must_change_password: bool,
updated_at: datetime,
revoke_sessions: bool,
) -> UserAccount: ...
def mark_user_login(self, user_id: str, *, now: datetime) -> UserAccount: ...
def create_user_session(self, session: UserSession) -> None: ...
def get_authenticated_user_session(
self,
token_digest: str,
*,
now: datetime,
) -> AuthenticatedUserSession | None: ...
def touch_user_session(
self,
session_id: str,
*,
last_seen_at: datetime,
idle_expires_at: datetime,
) -> UserSession: ...
def revoke_user_session(self, session_id: str, *, revoked_at: datetime) -> bool: ...
def revoke_user_sessions(self, user_id: str, *, revoked_at: datetime) -> int: ...
def get_login_throttle(
self,
username_normalized: str,
client_bucket: str,
) -> LoginThrottle | None: ...
def record_login_failure(
self,
*,
username_normalized: str,
client_bucket: str,
now: datetime,
failure_limit: int,
failure_window: timedelta,
block_duration: timedelta,
) -> LoginThrottle: ...
def clear_login_throttle(self, username_normalized: str, client_bucket: str) -> None: ...
def record_auth_audit(self, event: AuthAuditEvent) -> None: ...
def cleanup_auth_state(self, *, now: datetime, limit: int) -> int: ...
def list_reserved_device_ids(self, *, now: datetime) -> set[str]: ...
def assign_task(