Coverage for src/lexigram/admin/auth/protocols.py: 100%

102 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:39 +0800

1"""Admin authentication service protocols. 

2 

3All protocols remain in lexigram-admin (not lexigram-contracts) because they 

4are admin-specific and not consumed by other extension packages. 

5 

6``AdminAuditLogServiceProtocol`` extends the framework-wide 

7``AuditLoggerProtocol`` from ``lexigram.contracts.audit`` so that admin audit 

8implementations satisfy the cross-package contract. 

9""" 

10 

11from __future__ import annotations 

12 

13from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable 

14 

15from lexigram.contracts.audit import AuditLoggerProtocol 

16 

17if TYPE_CHECKING: 

18 from datetime import datetime 

19 

20 from lexigram.admin.auth.errors import AdminAuthError 

21 from lexigram.admin.auth.types import ( 

22 AdminAuthResult, 

23 AdminLockoutInfo, 

24 AdminLoginAttempt, 

25 AdminPasswordResetToken, 

26 AdminPasswordValidationResult, 

27 AdminSecurityEvent, 

28 AdminSecurityEventType, 

29 ) 

30 from lexigram.result import Result 

31 

32 

33@runtime_checkable 

34class AdminAuthServiceProtocol(Protocol): 

35 """Main authentication orchestration service protocol. 

36 

37 Coordinates credential verification, rate limiting, lockout checks, 

38 session issuance, and audit logging. 

39 """ 

40 

41 async def authenticate( 

42 self, 

43 email: str, 

44 password: str, 

45 ip_address: str, 

46 user_agent: str, 

47 ) -> Result[AdminAuthResult, AdminAuthError]: 

48 """Authenticate an admin user with full security pipeline. 

49 

50 Args: 

51 email: Admin user email. 

52 password: Plain-text password. 

53 ip_address: Client IP for rate limiting. 

54 user_agent: Client user agent for audit. 

55 

56 Returns: 

57 Ok(AdminAuthResult) with session details on success. 

58 Err with specific AdminAuthError subclass on failure. 

59 """ 

60 ... 

61 

62 async def invalidate_session(self, session_id: str) -> None: 

63 """Invalidate a session (logout). 

64 

65 Args: 

66 session_id: Session identifier to revoke. 

67 """ 

68 ... 

69 

70 async def invalidate_all_user_sessions(self, user_id: str) -> None: 

71 """Revoke all active sessions for a user (e.g., after password change). 

72 

73 Args: 

74 user_id: Admin user UUID whose sessions to revoke. 

75 """ 

76 ... 

77 

78 async def complete_mfa_login( 

79 self, 

80 user_id: str, 

81 email: str, 

82 roles: list[str], 

83 code: str, 

84 ip_address: str, 

85 user_agent: str, 

86 ) -> Result[AdminAuthResult, AdminAuthError]: 

87 """Complete a login after a successful TOTP challenge. 

88 

89 Verifies the code, then runs the post-credential pipeline 

90 (attempt recording, lockout clearance, session creation, audits) 

91 that was deferred when ``authenticate`` returned ``mfa_required``. 

92 

93 Args: 

94 user_id: Admin user UUID (from the pending challenge). 

95 email: Admin user email (from the pending challenge). 

96 roles: Role names for the user (from the pending challenge). 

97 code: TOTP code to verify. 

98 ip_address: Client IP for rate limiting. 

99 user_agent: Client user agent for audit. 

100 

101 Returns: 

102 Ok(AdminAuthResult) with a real session on success. 

103 Err(MfaVerificationFailedError) when the code is invalid. 

104 Err(MfaNotEnabledError) when 2FA is unavailable. 

105 """ 

106 ... 

107 

108 

109@runtime_checkable 

110class AdminLoginAttemptStoreProtocol(Protocol): 

111 """Persistence protocol for login attempt records.""" 

112 

113 async def ensure_schema(self) -> None: 

114 """Create the admin_login_attempts table if it does not exist.""" 

115 ... 

116 

117 async def insert(self, attempt: AdminLoginAttempt) -> None: 

118 """Persist a login attempt record. 

119 

120 Args: 

121 attempt: The attempt to store. 

122 """ 

123 ... 

124 

125 async def count_recent_failures(self, email: str, since_seconds: int) -> int: 

126 """Count failed attempts for email within the given window. 

127 

128 Args: 

129 email: Email address to query. 

130 since_seconds: Look-back window in seconds. 

131 

132 Returns: 

133 Number of failed attempts. 

134 """ 

135 ... 

136 

137 async def count_recent_failures_by_ip( 

138 self, ip_address: str, since_seconds: int 

139 ) -> int: 

140 """Count failed attempts from an IP within the given window. 

141 

142 Args: 

143 ip_address: IP address to query. 

144 since_seconds: Look-back window in seconds. 

145 

146 Returns: 

147 Number of failed attempts. 

148 """ 

149 ... 

150 

151 async def clear_failures(self, email: str) -> None: 

152 """Clear failure records for email (called on successful login). 

153 

154 Args: 

155 email: Email to clear. 

156 """ 

157 ... 

158 

159 

160@runtime_checkable 

161class AdminAccountLockoutStoreProtocol(Protocol): 

162 """Persistence protocol for account lockout records.""" 

163 

164 async def ensure_schema(self) -> None: 

165 """Create the admin_account_lockouts table if it does not exist.""" 

166 ... 

167 

168 async def get_active_lockout(self, email: str) -> AdminLockoutInfo | None: 

169 """Get active lockout for email, or None if not locked. 

170 

171 Args: 

172 email: Email to check. 

173 

174 Returns: 

175 AdminLockoutInfo if active lockout exists, None otherwise. 

176 """ 

177 ... 

178 

179 async def create_lockout( 

180 self, 

181 email: str, 

182 consecutive_failures: int, 

183 unlock_at: Any | None, 

184 is_permanent: bool, 

185 ) -> None: 

186 """Create or update a lockout record for email. 

187 

188 Args: 

189 email: Email to lock. 

190 consecutive_failures: Total consecutive failures. 

191 unlock_at: UTC datetime when lock expires (None if permanent). 

192 is_permanent: Whether this requires manual admin unlock. 

193 """ 

194 ... 

195 

196 async def clear_lockout(self, email: str) -> None: 

197 """Remove active lockout for email (on successful login or admin unlock). 

198 

199 Args: 

200 email: Email to unlock. 

201 """ 

202 ... 

203 

204 

205@runtime_checkable 

206class AdminLoginAttemptServiceProtocol(Protocol): 

207 """Service for IP rate limiting and account lockout enforcement.""" 

208 

209 async def check_ip_rate_limit(self, ip_address: str) -> None: 

210 """Check IP rate limit. Raises RateLimitExceededError if exceeded. 

211 

212 Args: 

213 ip_address: Client IP to check. 

214 

215 Raises: 

216 RateLimitExceededError: When the IP is rate-limited. 

217 """ 

218 ... 

219 

220 async def check_account_lockout(self, email: str) -> None: 

221 """Check account lockout status. Raises AccountLockedError if locked. 

222 

223 Args: 

224 email: Email address to check. 

225 

226 Raises: 

227 AccountLockedError: When the account is locked. 

228 """ 

229 ... 

230 

231 async def record_attempt( 

232 self, 

233 email: str, 

234 ip_address: str, 

235 user_agent: str, 

236 success: bool, 

237 failure_reason: str | None = None, 

238 ) -> None: 

239 """Record a login attempt and update lockout state on failure. 

240 

241 Args: 

242 email: Email that attempted login. 

243 ip_address: Client IP. 

244 user_agent: Client user agent. 

245 success: Whether the attempt succeeded. 

246 failure_reason: Short failure code when success=False. 

247 """ 

248 ... 

249 

250 async def clear_lockout(self, email: str) -> None: 

251 """Clear lockout and failure records on successful login. 

252 

253 Args: 

254 email: Email to clear. 

255 """ 

256 ... 

257 

258 

259@runtime_checkable 

260class AdminAuditLogStoreProtocol(Protocol): 

261 """Persistence protocol for security audit log entries.""" 

262 

263 async def ensure_schema(self) -> None: 

264 """Create the admin_security_audit_log table if it does not exist.""" 

265 ... 

266 

267 async def insert(self, event: AdminSecurityEvent) -> None: 

268 """Persist a security event. 

269 

270 Args: 

271 event: Security event to store. 

272 """ 

273 ... 

274 

275 async def query_recent( 

276 self, 

277 admin_user_id: str | None = None, 

278 event_type: AdminSecurityEventType | None = None, 

279 since_seconds: int = 3600, 

280 limit: int = 100, 

281 ) -> list[AdminSecurityEvent]: 

282 """Query recent security events with optional filters. 

283 

284 Args: 

285 admin_user_id: Filter to specific user (None = all users). 

286 event_type: Filter to specific event type (None = all types). 

287 since_seconds: Look-back window in seconds. 

288 limit: Maximum records to return. 

289 

290 Returns: 

291 List of matching security events, newest first. 

292 """ 

293 ... 

294 

295 

296@runtime_checkable 

297class AdminAuditLogServiceProtocol(AuditLoggerProtocol, Protocol): 

298 """Service for recording admin security events. 

299 

300 Extends the framework-wide ``AuditLoggerProtocol`` so that admin audit 

301 implementations satisfy the cross-package contract. Adds admin-specific 

302 methods (``log_event``, ``get_recent_events``) on top of the base 

303 ``log()`` and ``query()`` methods from ``AuditLoggerProtocol``. 

304 

305 Implementations must never raise — audit failures are swallowed so that 

306 an audit store outage cannot interrupt authentication flows. 

307 """ 

308 

309 async def log_event( 

310 self, 

311 event_type: AdminSecurityEventType, 

312 ip_address: str, 

313 user_agent: str, 

314 success: bool, 

315 admin_user_id: str | None = None, 

316 metadata: dict[str, Any] | None = None, 

317 ) -> None: 

318 """Record a security event. Implementation must never raise. 

319 

320 Args: 

321 event_type: Type of security event. 

322 ip_address: Client IP. 

323 user_agent: Client user agent. 

324 success: Whether the operation succeeded. 

325 admin_user_id: Associated admin user (None for pre-auth events). 

326 metadata: Optional structured context. 

327 """ 

328 ... 

329 

330 async def get_recent_events( 

331 self, 

332 admin_user_id: str | None = None, 

333 since_seconds: int = 3600, 

334 limit: int = 50, 

335 ) -> list[AdminSecurityEvent]: 

336 """Retrieve recent security events for display. 

337 

338 Args: 

339 admin_user_id: Filter to specific user. 

340 since_seconds: Look-back window. 

341 limit: Maximum results. 

342 

343 Returns: 

344 List of security events, newest first. 

345 """ 

346 ... 

347 

348 

349@runtime_checkable 

350class AdminPasswordPolicyServiceProtocol(Protocol): 

351 """Password policy validation service.""" 

352 

353 def validate( 

354 self, 

355 password: str, 

356 email: str | None = None, 

357 ) -> AdminPasswordValidationResult: 

358 """Validate a password against all configured policy rules. 

359 

360 Returns ALL violations, not just the first one. 

361 

362 Args: 

363 password: Plain-text password to validate. 

364 email: Optional email — used to check if password contains it. 

365 

366 Returns: 

367 AdminPasswordValidationResult with is_valid and full violations list. 

368 """ 

369 ... 

370 

371 

372@runtime_checkable 

373class AdminCsrfServiceProtocol(Protocol): 

374 """CSRF token generation and validation service.""" 

375 

376 def generate_token(self, session_id: str) -> str: 

377 """Generate a CSRF token scoped to the given session. 

378 

379 Token format: base64url(timestamp:nonce:hmac_signature) 

380 

381 Args: 

382 session_id: Session ID to scope the token to. 

383 

384 Returns: 

385 CSRF token string. 

386 """ 

387 ... 

388 

389 def validate_token(self, session_id: str, token: str) -> bool: 

390 """Validate a CSRF token against the session. 

391 

392 Uses hmac.compare_digest for timing-safe comparison. 

393 

394 Args: 

395 session_id: Session ID the token was generated for. 

396 token: Token to validate. 

397 

398 Returns: 

399 True if valid and not expired, False otherwise. 

400 """ 

401 ... 

402 

403 

404@runtime_checkable 

405class AdminSessionServiceProtocol(Protocol): 

406 """Admin session lifecycle management service.""" 

407 

408 async def create_session( 

409 self, 

410 user_id: str, 

411 email: str, 

412 roles: list[str], 

413 ip_address: str, 

414 user_agent: str, 

415 ) -> str: 

416 """Create a new session and return the session ID. 

417 

418 Args: 

419 user_id: Admin user UUID. 

420 email: Admin user email. 

421 roles: User's roles. 

422 ip_address: Client IP. 

423 user_agent: Client user agent. 

424 

425 Returns: 

426 New session identifier (secrets.token_urlsafe(32)). 

427 """ 

428 ... 

429 

430 async def get_session(self, session_id: str) -> dict[str, Any] | None: 

431 """Retrieve session data if valid (not expired, not revoked). 

432 

433 Checks both idle timeout and absolute expiry. 

434 

435 Args: 

436 session_id: Session to retrieve. 

437 

438 Returns: 

439 Session data dict or None if not found/expired. 

440 """ 

441 ... 

442 

443 async def touch_session(self, session_id: str) -> None: 

444 """Update session last-active timestamp. 

445 

446 Args: 

447 session_id: Session to touch. 

448 """ 

449 ... 

450 

451 async def revoke_session(self, session_id: str) -> None: 

452 """Revoke a single session. 

453 

454 Args: 

455 session_id: Session to revoke. 

456 """ 

457 ... 

458 

459 async def revoke_all_user_sessions(self, user_id: str) -> None: 

460 """Revoke all sessions for a user. 

461 

462 Args: 

463 user_id: Admin user UUID. 

464 """ 

465 ... 

466 

467 

468@runtime_checkable 

469class AdminPasswordResetTokenStoreProtocol(Protocol): 

470 """Persistence contract for password reset tokens. 

471 

472 Implementations: 

473 - :class:`~lexigram.admin.auth.store.password_reset_token_sql.AdminPasswordResetTokenSqlStore` 

474 """ 

475 

476 async def ensure_schema(self) -> None: 

477 """Create the token table if it does not exist.""" 

478 ... 

479 

480 async def create(self, email: str, token_hash: str, expires_at: datetime) -> None: 

481 """Persist a new token record. 

482 

483 Args: 

484 email: Email the token is issued for. 

485 token_hash: sha256 hex digest of the raw token. 

486 expires_at: UTC expiry timestamp. 

487 """ 

488 ... 

489 

490 async def find_by_hash(self, token_hash: str) -> AdminPasswordResetToken | None: 

491 """Look up a token by its sha256 hash. 

492 

493 Args: 

494 token_hash: sha256 hex digest of the raw token. 

495 

496 Returns: 

497 Token record or ``None`` when unknown. 

498 """ 

499 ... 

500 

501 async def mark_consumed(self, token_hash: str) -> bool: 

502 """Atomically verify-and-consume a token in one statement. 

503 

504 Args: 

505 token_hash: sha256 hex digest of the raw token. 

506 

507 Returns: 

508 ``True`` only when the token existed, was unconsumed, and had 

509 not expired at the instant of the write; ``False`` otherwise 

510 — the caller cannot distinguish missing, already-consumed, 

511 or expired without a separate lookup. 

512 """ 

513 ... 

514 

515 

516@runtime_checkable 

517class AdminMfaStoreProtocol(Protocol): 

518 """Persistence contract for per-user TOTP secrets. 

519 

520 Implementations: 

521 - :class:`~lexigram.admin.auth.store.mfa_sql.AdminMfaSqlStore` 

522 """ 

523 

524 async def ensure_schema(self) -> None: 

525 """Create the MFA table if it does not exist.""" 

526 ... 

527 

528 async def is_enabled(self, user_id: str) -> bool: 

529 """Return True when 2FA is enabled for the user.""" 

530 ... 

531 

532 async def get_secret(self, user_id: str) -> str | None: 

533 """Return the stored TOTP secret (None when disabled).""" 

534 ... 

535 

536 async def save_secret(self, user_id: str, secret: str) -> None: 

537 """Persist (or refresh) the TOTP secret for a user.""" 

538 ... 

539 

540 async def disable(self, user_id: str) -> None: 

541 """Remove the TOTP secret (2FA off).""" 

542 ... 

543 

544 

545@runtime_checkable 

546class AdminMfaServiceProtocol(Protocol): 

547 """TOTP 2FA orchestration contract. 

548 

549 Implementations: 

550 - :class:`~lexigram.admin.auth.services.mfa_service.AdminMfaService` 

551 """ 

552 

553 async def is_enabled(self, user_id: str) -> bool: 

554 """Return True when 2FA is enabled for the user.""" 

555 ... 

556 

557 async def start_setup( 

558 self, user_id: str, email: str 

559 ) -> Result[tuple[str, str, str], AdminAuthError]: 

560 """Generate a TOTP secret, provisioning URI, and QR SVG (no persist). 

561 

562 Returns: 

563 ``Ok((secret, otpauth_uri, svg))`` on success; ``Err`` when 2FA 

564 is disabled in configuration. 

565 """ 

566 ... 

567 

568 async def confirm_setup( 

569 self, user_id: str, secret: str, code: str 

570 ) -> Result[None, AdminAuthError]: 

571 """Validate a code against a new secret and persist it.""" 

572 ... 

573 

574 async def verify_code( 

575 self, user_id: str, code: str 

576 ) -> Result[bool, AdminAuthError]: 

577 """Validate a TOTP code; ``Err`` when 2FA is not enabled.""" 

578 ... 

579 

580 async def disable(self, user_id: str, code: str) -> Result[bool, AdminAuthError]: 

581 """Disable 2FA (requires a valid current code).""" 

582 ... 

583 

584 def get_factor(self) -> str: 

585 """Return the configured second factor (``"totp"`` or ``"email"``).""" 

586 ... 

587 

588 

589@runtime_checkable 

590class AdminEmailVerificationStoreProtocol(Protocol): 

591 """Persistence contract for admin email verification state. 

592 

593 Implementations: 

594 - :class:`~lexigram.admin.auth.store.email_verification_sql.AdminEmailVerificationSqlStore` 

595 """ 

596 

597 async def ensure_schema(self) -> None: 

598 """Create the verification table if it does not exist.""" 

599 ... 

600 

601 async def is_verified(self, user_id: str) -> bool: 

602 """Return True when the user's email is verified. 

603 

604 Args: 

605 user_id: Admin user UUID. 

606 """ 

607 ... 

608 

609 async def find_user_by_token_hash(self, token_hash: str) -> str | None: 

610 """Look up the user owning an unconsumed token. 

611 

612 Args: 

613 token_hash: sha256 hex digest of the raw token. 

614 

615 Returns: 

616 User UUID or ``None`` when no unconsumed token matches. 

617 """ 

618 ... 

619 

620 async def save_token( 

621 self, user_id: str, token_hash: str, expires_at: datetime 

622 ) -> None: 

623 """Persist (or refresh) the verification token for a user. 

624 

625 Args: 

626 user_id: Admin user UUID. 

627 token_hash: sha256 hex digest of the raw token. 

628 expires_at: UTC expiry timestamp. 

629 """ 

630 ... 

631 

632 async def consume_token(self, user_id: str, token_hash: str) -> bool: 

633 """Atomically verify + consume a token. 

634 

635 Marks the email verified and clears the token when the hash matches, 

636 the token is unexpired, and the email is not already verified. 

637 

638 Args: 

639 user_id: Admin user UUID. 

640 token_hash: sha256 hex digest of the raw token. 

641 

642 Returns: 

643 ``True`` when the token was valid and consumed. 

644 """ 

645 ... 

646 

647 async def clear_token(self, user_id: str) -> None: 

648 """Remove the pending verification token for a user. 

649 

650 Args: 

651 user_id: Admin user UUID. 

652 """ 

653 ... 

654 

655 

656@runtime_checkable 

657class AdminEmailOtpStoreProtocol(Protocol): 

658 """Persistence contract for email one-time-password codes. 

659 

660 Implementations: 

661 - :class:`~lexigram.admin.auth.store.email_otp_sql.AdminEmailOtpSqlStore` 

662 """ 

663 

664 async def ensure_schema(self) -> None: 

665 """Create the OTP table if it does not exist.""" 

666 ... 

667 

668 async def save(self, user_id: str, code_hash: str, expires_at: datetime) -> None: 

669 """Persist a new emailed code. 

670 

671 Args: 

672 user_id: Admin user UUID. 

673 code_hash: sha256 hex digest of the raw code. 

674 expires_at: UTC expiry timestamp. 

675 """ 

676 ... 

677 

678 async def consume(self, user_id: str, code_hash: str) -> bool: 

679 """Atomically consume a matching unexpired code. 

680 

681 Args: 

682 user_id: Admin user UUID. 

683 code_hash: sha256 hex digest of the raw code. 

684 

685 Returns: 

686 ``True`` when an unexpired, unused code matched and was consumed. 

687 """ 

688 ... 

689 

690 async def last_sent_at(self, user_id: str) -> datetime | None: 

691 """Return the creation time of the most recent code. 

692 

693 Args: 

694 user_id: Admin user UUID. 

695 

696 Returns: 

697 UTC datetime of the newest code, or ``None`` when none exists. 

698 """ 

699 ... 

700 

701 

702@runtime_checkable 

703class AdminEmailOtpServiceProtocol(Protocol): 

704 """Email one-time-password factor contract. 

705 

706 Implementations: 

707 - :class:`~lexigram.admin.auth.services.email_otp_service.AdminEmailOtpService` 

708 """ 

709 

710 async def send_otp( 

711 self, user_id: str, email: str, user_name: str 

712 ) -> Result[None, AdminAuthError]: 

713 """Generate, persist, and email a fresh one-time code. 

714 

715 Returns: 

716 ``Ok(None)`` on success; ``Err`` when disabled, in cooldown, or 

717 undeliverable. 

718 """ 

719 ... 

720 

721 async def verify_otp(self, user_id: str, code: str) -> Result[bool, AdminAuthError]: 

722 """Verify a code and consume it when valid. 

723 

724 Returns: 

725 ``Ok(True)`` on match; ``Ok(False)`` otherwise; 

726 ``Err`` when the factor is disabled. 

727 """ 

728 ... 

729 

730 

731@runtime_checkable 

732class AdminEmailVerificationServiceProtocol(Protocol): 

733 """Email verification orchestration contract. 

734 

735 Implementations: 

736 - :class:`~lexigram.admin.auth.services.email_verification_service.AdminEmailVerificationService` 

737 """ 

738 

739 async def is_verified(self, user_id: str) -> bool: 

740 """Return True when the user's email is verified.""" 

741 ... 

742 

743 async def is_required(self, user_id: str) -> bool: 

744 """Return True when login must be gated on email verification.""" 

745 ... 

746 

747 async def send_verification( 

748 self, 

749 user_id: str, 

750 email: str, 

751 user_name: str, 

752 base_url: str = "", 

753 ip_address: str = "", 

754 ) -> Result[None, AdminAuthError]: 

755 """Issue a verification link and email it to the user. 

756 

757 No-op (Ok) when disabled or already verified; fail-open on delivery. 

758 Rate limited per IP when a cache backend is wired (fail open). 

759 """ 

760 ... 

761 

762 async def verify_token(self, token: str) -> Result[bool, AdminAuthError]: 

763 """Validate and consume a verification token. 

764 

765 Returns: 

766 ``Ok(True)`` on success; ``Err(EmailVerificationTokenInvalidError)`` 

767 for unknown/used/expired tokens. 

768 """ 

769 ... 

770 

771 async def resend_verification( 

772 self, 

773 user_id: str, 

774 email: str, 

775 user_name: str, 

776 base_url: str = "", 

777 ip_address: str = "", 

778 ) -> Result[None, AdminAuthError]: 

779 """Re-issue and re-send the verification email.""" 

780 ... 

781 

782 

783@runtime_checkable 

784class AdminPasswordResetServiceProtocol(Protocol): 

785 """Password reset orchestration contract.""" 

786 

787 async def request_reset( 

788 self, 

789 email: str, 

790 ip_address: str, 

791 user_agent: str, 

792 base_url: str, 

793 ) -> Result[None, AdminAuthError]: 

794 """Issue a reset token and notify the user. 

795 

796 Always returns ``Ok(None)`` for unknown emails (anti-enumeration). 

797 """ 

798 ... 

799 

800 async def confirm_reset( 

801 self, 

802 token: str, 

803 new_password: str, 

804 ip_address: str = "", 

805 user_agent: str = "", 

806 ) -> Result[None, AdminAuthError]: 

807 """Validate a token and apply a new password. 

808 

809 Consumes the token on success and invalidates all user sessions. 

810 """ 

811 ... 

812 

813 

814__all__ = [ 

815 "AdminAccountLockoutStoreProtocol", 

816 "AdminAuditLogServiceProtocol", 

817 "AdminAuditLogStoreProtocol", 

818 "AdminAuthServiceProtocol", 

819 "AdminCsrfServiceProtocol", 

820 "AdminEmailOtpServiceProtocol", 

821 "AdminEmailOtpStoreProtocol", 

822 "AdminEmailVerificationServiceProtocol", 

823 "AdminEmailVerificationStoreProtocol", 

824 "AdminLoginAttemptServiceProtocol", 

825 "AdminLoginAttemptStoreProtocol", 

826 "AdminMfaServiceProtocol", 

827 "AdminMfaStoreProtocol", 

828 "AdminPasswordPolicyServiceProtocol", 

829 "AdminPasswordResetServiceProtocol", 

830 "AdminPasswordResetTokenStoreProtocol", 

831 "AdminSessionServiceProtocol", 

832]