015c622016
Add 162 integration tests covering authentication flows, TOTP MFA, SSH key/certificate management, organization workflows, multi-org access, self-service features, admin operations, authorization, security edge cases, department/principal management, CA management, policy compliance, WebAuthn passkeys, and ZeroTier network access. Includes: - Reusable API client library with session management - Test fixtures for users, organizations, memberships, and CAs - Helper functions for SSH key generation and verification - Documentation for running and writing tests Also update test configuration to disable conflicting maas plugins and configure WebAuthn/session settings for localhost testing.
96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
"""MFA (TOTP) client for integration tests."""
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MfaClient:
|
|
"""Wraps TOTP MFA-related API calls."""
|
|
|
|
def __init__(self, client):
|
|
self._client = client
|
|
|
|
# ------------------------------------------------------------------
|
|
# TOTP Enrollment
|
|
# ------------------------------------------------------------------
|
|
def enroll_totp(self) -> dict:
|
|
"""Begin TOTP enrollment.
|
|
|
|
Returns:
|
|
Response dict containing ``secret``, ``provisioning_uri``,
|
|
``qr_code``, and ``backup_codes``.
|
|
"""
|
|
logger.info("[MfaClient] Enrolling TOTP")
|
|
return self._client.post("/auth/totp/enroll")
|
|
|
|
def verify_enrollment(self, code: str, client_timestamp: str | None = None) -> dict:
|
|
"""Complete TOTP enrollment by verifying the first code.
|
|
|
|
Args:
|
|
code: 6-digit TOTP code generated from the secret.
|
|
client_timestamp: Optional ISO-8601 timestamp for drift calc.
|
|
"""
|
|
payload = {"code": code}
|
|
if client_timestamp:
|
|
payload["client_timestamp"] = client_timestamp
|
|
logger.info("[MfaClient] Verifying TOTP enrollment")
|
|
return self._client.post("/auth/totp/verify-enrollment", data=payload)
|
|
|
|
# ------------------------------------------------------------------
|
|
# TOTP Verification (during login)
|
|
# ------------------------------------------------------------------
|
|
def verify_totp(self, code: str, is_backup_code: bool = False, client_timestamp: str | None = None) -> dict:
|
|
"""Verify TOTP code during the multi-step login flow.
|
|
|
|
This is called AFTER ``AuthClient.login`` returns
|
|
``requires_totp=True`` and stores the pending user id in the
|
|
server-side session.
|
|
|
|
Args:
|
|
code: 6-digit TOTP code or backup code.
|
|
is_backup_code: True if ``code`` is a backup code.
|
|
client_timestamp: Optional ISO-8601 timestamp.
|
|
|
|
Returns:
|
|
Response dict containing ``user``, ``token``, ``expires_at``.
|
|
"""
|
|
payload = {"code": code, "is_backup_code": is_backup_code}
|
|
if client_timestamp:
|
|
payload["client_timestamp"] = client_timestamp
|
|
logger.info(f"[MfaClient] Verifying TOTP — backup={is_backup_code}")
|
|
result = self._client.post("/auth/totp/verify", data=payload)
|
|
token = result.get("data", {}).get("token")
|
|
if token:
|
|
self._client.set_token(token)
|
|
logger.info("[MfaClient] TOTP verification successful — token stored")
|
|
return result
|
|
|
|
# ------------------------------------------------------------------
|
|
# TOTP Management
|
|
# ------------------------------------------------------------------
|
|
def get_totp_status(self) -> dict:
|
|
"""Return current TOTP status and remaining backup codes."""
|
|
return self._client.get("/auth/totp/status")
|
|
|
|
def disable_totp(self, password: str) -> dict:
|
|
"""Disable TOTP for the current user.
|
|
|
|
Args:
|
|
password: Current account password (required for confirmation).
|
|
"""
|
|
return self._client.delete("/auth/totp/disable", data={"password": password})
|
|
|
|
def regenerate_backup_codes(self, password: str) -> dict:
|
|
"""Generate a fresh set of backup codes.
|
|
|
|
Args:
|
|
password: Current account password (required for confirmation).
|
|
|
|
Returns:
|
|
Response dict containing ``backup_codes``.
|
|
"""
|
|
return self._client.post(
|
|
"/auth/totp/regenerate-backup-codes",
|
|
data={"password": password},
|
|
)
|