ariadne: add password-rotation checks + tune schedules
This commit is contained in:
parent
8578524e56
commit
b45911c50e
@ -19,6 +19,7 @@ from .mailu import mailu
|
|||||||
HTTP_OK = 200
|
HTTP_OK = 200
|
||||||
FIREFLY_PASSWORD_ATTR = "firefly_password"
|
FIREFLY_PASSWORD_ATTR = "firefly_password"
|
||||||
FIREFLY_PASSWORD_UPDATED_ATTR = "firefly_password_updated_at"
|
FIREFLY_PASSWORD_UPDATED_ATTR = "firefly_password_updated_at"
|
||||||
|
FIREFLY_PASSWORD_ROTATED_ATTR = "firefly_password_rotated_at"
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -141,11 +142,109 @@ _FIREFLY_SYNC_SCRIPT = textwrap.dedent(
|
|||||||
"""
|
"""
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
_FIREFLY_PASSWORD_CHECK_SCRIPT = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use FireflyIII\\Support\\Facades\\FireflyConfig;
|
||||||
|
use FireflyIII\\User;
|
||||||
|
use Illuminate\\Contracts\\Console\\Kernel as ConsoleKernel;
|
||||||
|
use Illuminate\\Support\\Facades\\Hash;
|
||||||
|
|
||||||
|
function log_line(string $message): void
|
||||||
|
{
|
||||||
|
fwrite(STDOUT, $message . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function error_line(string $message): void
|
||||||
|
{
|
||||||
|
fwrite(STDERR, $message . PHP_EOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function find_app_root(): string
|
||||||
|
{
|
||||||
|
$candidates = [];
|
||||||
|
$env_root = getenv('FIREFLY_APP_DIR') ?: '';
|
||||||
|
if ($env_root !== '') {
|
||||||
|
$candidates[] = $env_root;
|
||||||
|
}
|
||||||
|
$candidates[] = '/var/www/html';
|
||||||
|
$candidates[] = '/var/www/firefly-iii';
|
||||||
|
$candidates[] = '/app';
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if (!is_dir($candidate)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file_exists($candidate . '/vendor/autoload.php')) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$email = trim((string) getenv('FIREFLY_USER_EMAIL'));
|
||||||
|
$password = (string) getenv('FIREFLY_USER_PASSWORD');
|
||||||
|
|
||||||
|
if ($email === '' || $password === '') {
|
||||||
|
error_line('missing FIREFLY_USER_EMAIL or FIREFLY_USER_PASSWORD');
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = find_app_root();
|
||||||
|
if ($root === '') {
|
||||||
|
error_line('firefly app root not found');
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoload = $root . '/vendor/autoload.php';
|
||||||
|
$app_bootstrap = $root . '/bootstrap/app.php';
|
||||||
|
|
||||||
|
if (!file_exists($autoload) || !file_exists($app_bootstrap)) {
|
||||||
|
error_line('firefly bootstrap files missing');
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
require $autoload;
|
||||||
|
$app = require $app_bootstrap;
|
||||||
|
|
||||||
|
$kernel = $app->make(ConsoleKernel::class);
|
||||||
|
$kernel->bootstrap();
|
||||||
|
|
||||||
|
try {
|
||||||
|
FireflyConfig::set('single_user_mode', true);
|
||||||
|
} catch (Throwable $exc) {
|
||||||
|
error_line('failed to enforce single_user_mode: ' . $exc->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$existing_user = User::where('email', $email)->first();
|
||||||
|
if (!$existing_user) {
|
||||||
|
error_line('firefly user missing');
|
||||||
|
exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Hash::check($password, $existing_user->password)) {
|
||||||
|
log_line('password match');
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_line('password mismatch');
|
||||||
|
exit(1);
|
||||||
|
"""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
def _firefly_exec_command() -> str:
|
def _firefly_exec_command() -> str:
|
||||||
return f"php <<'PHP'\n{_FIREFLY_SYNC_SCRIPT}\nPHP"
|
return f"php <<'PHP'\n{_FIREFLY_SYNC_SCRIPT}\nPHP"
|
||||||
|
|
||||||
|
|
||||||
|
def _firefly_check_command() -> str:
|
||||||
|
return f"php <<'PHP'\n{_FIREFLY_PASSWORD_CHECK_SCRIPT}\nPHP"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FireflySyncSummary:
|
class FireflySyncSummary:
|
||||||
processed: int
|
processed: int
|
||||||
@ -194,6 +293,7 @@ class FireflySyncInput:
|
|||||||
password: str
|
password: str
|
||||||
password_generated: bool
|
password_generated: bool
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
rotated_at: str
|
||||||
|
|
||||||
|
|
||||||
def _extract_attr(attrs: Any, key: str) -> str:
|
def _extract_attr(attrs: Any, key: str) -> str:
|
||||||
@ -264,6 +364,15 @@ def _set_firefly_updated_at(username: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _set_firefly_rotated_at(username: str) -> bool:
|
||||||
|
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
try:
|
||||||
|
keycloak_admin.set_user_attribute(username, FIREFLY_PASSWORD_ROTATED_ATTR, now_iso)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _normalize_user(user: dict[str, Any]) -> tuple[UserSyncOutcome | None, UserIdentity | None]:
|
def _normalize_user(user: dict[str, Any]) -> tuple[UserSyncOutcome | None, UserIdentity | None]:
|
||||||
username = user.get("username") if isinstance(user.get("username"), str) else ""
|
username = user.get("username") if isinstance(user.get("username"), str) else ""
|
||||||
if _should_skip_user(user, username):
|
if _should_skip_user(user, username):
|
||||||
@ -339,12 +448,14 @@ def _build_sync_input(user: dict[str, Any]) -> FireflySyncInput | UserSyncOutcom
|
|||||||
return UserSyncOutcome("failed", "missing firefly password")
|
return UserSyncOutcome("failed", "missing firefly password")
|
||||||
|
|
||||||
updated_at = _extract_attr(attrs, FIREFLY_PASSWORD_UPDATED_ATTR)
|
updated_at = _extract_attr(attrs, FIREFLY_PASSWORD_UPDATED_ATTR)
|
||||||
|
rotated_at = _extract_attr(attrs, FIREFLY_PASSWORD_ROTATED_ATTR)
|
||||||
return FireflySyncInput(
|
return FireflySyncInput(
|
||||||
username=identity.username,
|
username=identity.username,
|
||||||
mailu_email=mailu_email,
|
mailu_email=mailu_email,
|
||||||
password=password,
|
password=password,
|
||||||
password_generated=password_generated,
|
password_generated=password_generated,
|
||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
|
rotated_at=rotated_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -383,6 +494,39 @@ class FireflyService:
|
|||||||
output = (result.stdout or result.stderr).strip()
|
output = (result.stdout or result.stderr).strip()
|
||||||
return {"status": "ok", "detail": output}
|
return {"status": "ok", "detail": output}
|
||||||
|
|
||||||
|
def check_password(self, email: str, password: str) -> dict[str, Any]:
|
||||||
|
email = (email or "").strip()
|
||||||
|
if not email:
|
||||||
|
raise RuntimeError("missing email")
|
||||||
|
if not password:
|
||||||
|
raise RuntimeError("missing password")
|
||||||
|
if not settings.firefly_namespace:
|
||||||
|
raise RuntimeError("firefly sync not configured")
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"FIREFLY_USER_EMAIL": email,
|
||||||
|
"FIREFLY_USER_PASSWORD": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self._executor.exec(
|
||||||
|
_firefly_check_command(),
|
||||||
|
env=env,
|
||||||
|
timeout_sec=settings.firefly_user_sync_wait_timeout_sec,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (ExecError, PodSelectionError, TimeoutError) as exc:
|
||||||
|
return {"status": "error", "detail": str(exc)}
|
||||||
|
|
||||||
|
detail = (result.stdout or result.stderr).strip()
|
||||||
|
if result.exit_code == 0:
|
||||||
|
return {"status": "match", "detail": detail}
|
||||||
|
if result.exit_code == 1:
|
||||||
|
return {"status": "mismatch", "detail": detail}
|
||||||
|
if result.exit_code == 3:
|
||||||
|
return {"status": "missing", "detail": detail or "user missing"}
|
||||||
|
return {"status": "error", "detail": detail or f"exit_code={result.exit_code}"}
|
||||||
|
|
||||||
def run_cron(self) -> dict[str, Any]:
|
def run_cron(self) -> dict[str, Any]:
|
||||||
if not settings.firefly_cron_token:
|
if not settings.firefly_cron_token:
|
||||||
raise RuntimeError("firefly cron token missing")
|
raise RuntimeError("firefly cron token missing")
|
||||||
@ -401,7 +545,19 @@ class FireflyService:
|
|||||||
if isinstance(prepared, UserSyncOutcome):
|
if isinstance(prepared, UserSyncOutcome):
|
||||||
return prepared
|
return prepared
|
||||||
if _should_skip_sync(prepared.password_generated, prepared.updated_at):
|
if _should_skip_sync(prepared.password_generated, prepared.updated_at):
|
||||||
return UserSyncOutcome("skipped")
|
if prepared.rotated_at:
|
||||||
|
return UserSyncOutcome("skipped")
|
||||||
|
check = self.check_password(prepared.mailu_email, prepared.password)
|
||||||
|
status = check.get("status") if isinstance(check, dict) else "error"
|
||||||
|
if status == "match":
|
||||||
|
return UserSyncOutcome("skipped")
|
||||||
|
if status == "mismatch":
|
||||||
|
if not _set_firefly_rotated_at(prepared.username):
|
||||||
|
return UserSyncOutcome("failed", "failed to set rotated_at")
|
||||||
|
return UserSyncOutcome("synced")
|
||||||
|
detail = check.get("detail") if isinstance(check, dict) else ""
|
||||||
|
detail = detail or "password check failed"
|
||||||
|
return UserSyncOutcome("failed", detail)
|
||||||
|
|
||||||
result = self.sync_user(prepared.mailu_email, prepared.password, wait=True)
|
result = self.sync_user(prepared.mailu_email, prepared.password, wait=True)
|
||||||
result_status = result.get("status") if isinstance(result, dict) else "error"
|
result_status = result.get("status") if isinstance(result, dict) else "error"
|
||||||
|
|||||||
@ -54,21 +54,24 @@ class VaultwardenService:
|
|||||||
return [url for url in urls if url]
|
return [url for url in urls if url]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _already_present(resp: httpx.Response) -> bool:
|
def _invite_conflict_status(resp: httpx.Response) -> str | None:
|
||||||
try:
|
try:
|
||||||
body = resp.text or ""
|
body = resp.text or ""
|
||||||
except Exception:
|
except Exception:
|
||||||
body = ""
|
body = ""
|
||||||
lowered = body.lower()
|
lowered = body.lower()
|
||||||
return any(
|
if "already invited" in lowered:
|
||||||
|
return "invited"
|
||||||
|
if any(
|
||||||
marker in lowered
|
marker in lowered
|
||||||
for marker in (
|
for marker in (
|
||||||
"already invited",
|
|
||||||
"already exists",
|
"already exists",
|
||||||
"already registered",
|
"already registered",
|
||||||
"user already exists",
|
"user already exists",
|
||||||
)
|
)
|
||||||
)
|
):
|
||||||
|
return "already_present"
|
||||||
|
return None
|
||||||
|
|
||||||
def _invite_via(self, base_url: str, email: str) -> VaultwardenInvite | None:
|
def _invite_via(self, base_url: str, email: str) -> VaultwardenInvite | None:
|
||||||
if not base_url:
|
if not base_url:
|
||||||
@ -81,8 +84,14 @@ class VaultwardenService:
|
|||||||
result = self._rate_limited()
|
result = self._rate_limited()
|
||||||
elif resp.status_code in {HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT}:
|
elif resp.status_code in {HTTP_OK, HTTP_CREATED, HTTP_NO_CONTENT}:
|
||||||
result = VaultwardenInvite(ok=True, status="invited", detail="invite created")
|
result = VaultwardenInvite(ok=True, status="invited", detail="invite created")
|
||||||
elif resp.status_code in {HTTP_BAD_REQUEST, HTTP_CONFLICT} and self._already_present(resp):
|
elif resp.status_code in {HTTP_BAD_REQUEST, HTTP_CONFLICT}:
|
||||||
result = VaultwardenInvite(ok=True, status="already_present", detail="user already present")
|
status = self._invite_conflict_status(resp)
|
||||||
|
if status == "invited":
|
||||||
|
result = VaultwardenInvite(ok=True, status="invited", detail="user already invited")
|
||||||
|
elif status == "already_present":
|
||||||
|
result = VaultwardenInvite(ok=True, status="already_present", detail="user already present")
|
||||||
|
else:
|
||||||
|
result = VaultwardenInvite(ok=False, status="error", detail=f"status {resp.status_code}")
|
||||||
else:
|
else:
|
||||||
result = VaultwardenInvite(ok=False, status="error", detail=f"status {resp.status_code}")
|
result = VaultwardenInvite(ok=False, status="error", detail=f"status {resp.status_code}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from .vaultwarden import vaultwarden
|
|||||||
VAULTWARDEN_EMAIL_ATTR = "vaultwarden_email"
|
VAULTWARDEN_EMAIL_ATTR = "vaultwarden_email"
|
||||||
VAULTWARDEN_STATUS_ATTR = "vaultwarden_status"
|
VAULTWARDEN_STATUS_ATTR = "vaultwarden_status"
|
||||||
VAULTWARDEN_SYNCED_AT_ATTR = "vaultwarden_synced_at"
|
VAULTWARDEN_SYNCED_AT_ATTR = "vaultwarden_synced_at"
|
||||||
|
VAULTWARDEN_MASTER_ATTR = "vaultwarden_master_password_set_at"
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -161,11 +162,16 @@ def _has_pending_failures(users: list[dict[str, Any]]) -> bool:
|
|||||||
attrs = user.get("attributes") if isinstance(user.get("attributes"), dict) else {}
|
attrs = user.get("attributes") if isinstance(user.get("attributes"), dict) else {}
|
||||||
status = _extract_attr(attrs, VAULTWARDEN_STATUS_ATTR)
|
status = _extract_attr(attrs, VAULTWARDEN_STATUS_ATTR)
|
||||||
synced_at = _extract_attr(attrs, VAULTWARDEN_SYNCED_AT_ATTR)
|
synced_at = _extract_attr(attrs, VAULTWARDEN_SYNCED_AT_ATTR)
|
||||||
|
master_set_at = _extract_attr(attrs, VAULTWARDEN_MASTER_ATTR)
|
||||||
synced_ts = _parse_synced_at(synced_at)
|
synced_ts = _parse_synced_at(synced_at)
|
||||||
if not status:
|
if not status:
|
||||||
return True
|
return True
|
||||||
if status in {"invited", "already_present"} and not synced_at:
|
if status in {"invited", "already_present"} and not synced_at:
|
||||||
return True
|
return True
|
||||||
|
if status == "already_present" and not master_set_at:
|
||||||
|
return True
|
||||||
|
if status == "invited" and _should_refresh_invite(synced_ts):
|
||||||
|
return True
|
||||||
if status in {"error", "rate_limited"} and not _cooldown_active(status, synced_ts):
|
if status in {"error", "rate_limited"} and not _cooldown_active(status, synced_ts):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -183,6 +189,19 @@ def _set_sync_status(username: str, status: str) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _set_master_password_set(username: str, full_user: dict[str, Any]) -> None:
|
||||||
|
if _extract_attr(full_user.get("attributes"), VAULTWARDEN_MASTER_ATTR):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
_set_user_attribute(
|
||||||
|
username,
|
||||||
|
VAULTWARDEN_MASTER_ATTR,
|
||||||
|
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def _ensure_email_attrs(username: str, full_user: dict[str, Any], email: str) -> None:
|
def _ensure_email_attrs(username: str, full_user: dict[str, Any], email: str) -> None:
|
||||||
try:
|
try:
|
||||||
_set_user_attribute_if_missing(username, full_user, "mailu_email", email)
|
_set_user_attribute_if_missing(username, full_user, "mailu_email", email)
|
||||||
@ -191,18 +210,34 @@ def _ensure_email_attrs(username: str, full_user: dict[str, Any], email: str) ->
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def _should_refresh_invite(synced_ts: float | None) -> bool:
|
||||||
|
if synced_ts is None:
|
||||||
|
return True
|
||||||
|
return (time.time() - synced_ts) >= settings.vaultwarden_invite_refresh_sec
|
||||||
|
|
||||||
|
|
||||||
def _handle_existing_invite(
|
def _handle_existing_invite(
|
||||||
username: str,
|
username: str,
|
||||||
current_status: str,
|
current_status: str,
|
||||||
current_synced_at: str,
|
current_synced_at: str,
|
||||||
|
current_synced_ts: float | None,
|
||||||
|
full_user: dict[str, Any],
|
||||||
counters: VaultwardenSyncCounters,
|
counters: VaultwardenSyncCounters,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
if current_status not in {"invited", "already_present"}:
|
if current_status not in {"invited", "already_present"}:
|
||||||
return False
|
return False
|
||||||
if not current_synced_at:
|
if current_status == "already_present":
|
||||||
_set_sync_status(username, current_status)
|
if not current_synced_at:
|
||||||
counters.skipped += 1
|
_set_sync_status(username, current_status)
|
||||||
return True
|
_set_master_password_set(username, full_user)
|
||||||
|
counters.skipped += 1
|
||||||
|
return True
|
||||||
|
if not _should_refresh_invite(current_synced_ts):
|
||||||
|
if not current_synced_at:
|
||||||
|
_set_sync_status(username, current_status)
|
||||||
|
counters.skipped += 1
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _sync_user(
|
def _sync_user(
|
||||||
@ -227,7 +262,14 @@ def _sync_user(
|
|||||||
counters.skipped += 1
|
counters.skipped += 1
|
||||||
else:
|
else:
|
||||||
_ensure_email_attrs(username, full_user, email)
|
_ensure_email_attrs(username, full_user, email)
|
||||||
if _handle_existing_invite(username, current_status, current_synced_at, counters):
|
if _handle_existing_invite(
|
||||||
|
username,
|
||||||
|
current_status,
|
||||||
|
current_synced_at,
|
||||||
|
current_synced_ts,
|
||||||
|
full_user,
|
||||||
|
counters,
|
||||||
|
):
|
||||||
status = None
|
status = None
|
||||||
else:
|
else:
|
||||||
counters.processed += 1
|
counters.processed += 1
|
||||||
@ -239,6 +281,8 @@ def _sync_user(
|
|||||||
else:
|
else:
|
||||||
counters.failures += 1
|
counters.failures += 1
|
||||||
_set_sync_status(username, result.status)
|
_set_sync_status(username, result.status)
|
||||||
|
if result.status == "already_present":
|
||||||
|
_set_master_password_set(username, full_user)
|
||||||
return status, ok
|
return status, ok
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from .mailu import mailu
|
|||||||
|
|
||||||
WGER_PASSWORD_ATTR = "wger_password"
|
WGER_PASSWORD_ATTR = "wger_password"
|
||||||
WGER_PASSWORD_UPDATED_ATTR = "wger_password_updated_at"
|
WGER_PASSWORD_UPDATED_ATTR = "wger_password_updated_at"
|
||||||
|
WGER_PASSWORD_ROTATED_ATTR = "wger_password_rotated_at"
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@ -142,12 +143,67 @@ _WGER_SYNC_SCRIPT = textwrap.dedent(
|
|||||||
"""
|
"""
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
_WGER_PASSWORD_CHECK_SCRIPT = textwrap.dedent(
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
|
||||||
|
def _env(name: str, default: str = "") -> str:
|
||||||
|
value = os.getenv(name, default)
|
||||||
|
return value.strip() if isinstance(value, str) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_django() -> None:
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.main")
|
||||||
|
django.setup()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
username = _env("WGER_USERNAME")
|
||||||
|
password = _env("WGER_PASSWORD")
|
||||||
|
|
||||||
|
if not username or not password:
|
||||||
|
print("missing username or password")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
_setup_django()
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
user = User.objects.filter(username=username).first()
|
||||||
|
if not user:
|
||||||
|
print(f"user {username} missing")
|
||||||
|
return 3
|
||||||
|
|
||||||
|
if user.check_password(password):
|
||||||
|
print("password match")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print("password mismatch")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
|
"""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
def _wger_exec_command() -> str:
|
def _wger_exec_command() -> str:
|
||||||
bootstrap = "if [ -f /vault/secrets/wger-env ]; then . /vault/secrets/wger-env; fi"
|
bootstrap = ". /vault/secrets/wger-env >/dev/null 2>&1 || true"
|
||||||
return f"{bootstrap}\npython3 - <<'PY'\n{_WGER_SYNC_SCRIPT}\nPY"
|
return f"{bootstrap}\npython3 - <<'PY'\n{_WGER_SYNC_SCRIPT}\nPY"
|
||||||
|
|
||||||
|
|
||||||
|
def _wger_check_command() -> str:
|
||||||
|
bootstrap = ". /vault/secrets/wger-env >/dev/null 2>&1 || true"
|
||||||
|
return f"{bootstrap}\npython3 - <<'PY'\n{_WGER_PASSWORD_CHECK_SCRIPT}\nPY"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class WgerSyncSummary:
|
class WgerSyncSummary:
|
||||||
processed: int
|
processed: int
|
||||||
@ -196,6 +252,7 @@ class WgerSyncInput:
|
|||||||
password: str
|
password: str
|
||||||
password_generated: bool
|
password_generated: bool
|
||||||
updated_at: str
|
updated_at: str
|
||||||
|
rotated_at: str
|
||||||
|
|
||||||
|
|
||||||
def _extract_attr(attrs: Any, key: str) -> str:
|
def _extract_attr(attrs: Any, key: str) -> str:
|
||||||
@ -266,6 +323,15 @@ def _set_wger_updated_at(username: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _set_wger_rotated_at(username: str) -> bool:
|
||||||
|
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
try:
|
||||||
|
keycloak_admin.set_user_attribute(username, WGER_PASSWORD_ROTATED_ATTR, now_iso)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _normalize_user(user: dict[str, Any]) -> tuple[UserSyncOutcome | None, UserIdentity | None]:
|
def _normalize_user(user: dict[str, Any]) -> tuple[UserSyncOutcome | None, UserIdentity | None]:
|
||||||
username = user.get("username") if isinstance(user.get("username"), str) else ""
|
username = user.get("username") if isinstance(user.get("username"), str) else ""
|
||||||
if _should_skip_user(user, username):
|
if _should_skip_user(user, username):
|
||||||
@ -341,12 +407,14 @@ def _build_sync_input(user: dict[str, Any]) -> WgerSyncInput | UserSyncOutcome:
|
|||||||
return UserSyncOutcome("failed", "missing wger password")
|
return UserSyncOutcome("failed", "missing wger password")
|
||||||
|
|
||||||
updated_at = _extract_attr(attrs, WGER_PASSWORD_UPDATED_ATTR)
|
updated_at = _extract_attr(attrs, WGER_PASSWORD_UPDATED_ATTR)
|
||||||
|
rotated_at = _extract_attr(attrs, WGER_PASSWORD_ROTATED_ATTR)
|
||||||
return WgerSyncInput(
|
return WgerSyncInput(
|
||||||
username=identity.username,
|
username=identity.username,
|
||||||
mailu_email=mailu_email,
|
mailu_email=mailu_email,
|
||||||
password=password,
|
password=password,
|
||||||
password_generated=password_generated,
|
password_generated=password_generated,
|
||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
|
rotated_at=rotated_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -386,6 +454,39 @@ class WgerService:
|
|||||||
output = (result.stdout or result.stderr).strip()
|
output = (result.stdout or result.stderr).strip()
|
||||||
return {"status": "ok", "detail": output}
|
return {"status": "ok", "detail": output}
|
||||||
|
|
||||||
|
def check_password(self, username: str, password: str) -> dict[str, Any]:
|
||||||
|
username = (username or "").strip()
|
||||||
|
if not username:
|
||||||
|
raise RuntimeError("missing username")
|
||||||
|
if not password:
|
||||||
|
raise RuntimeError("missing password")
|
||||||
|
if not settings.wger_namespace:
|
||||||
|
raise RuntimeError("wger sync not configured")
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"WGER_USERNAME": username,
|
||||||
|
"WGER_PASSWORD": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self._executor.exec(
|
||||||
|
_wger_check_command(),
|
||||||
|
env=env,
|
||||||
|
timeout_sec=settings.wger_user_sync_wait_timeout_sec,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (ExecError, PodSelectionError, TimeoutError) as exc:
|
||||||
|
return {"status": "error", "detail": str(exc)}
|
||||||
|
|
||||||
|
detail = (result.stdout or result.stderr).strip()
|
||||||
|
if result.exit_code == 0:
|
||||||
|
return {"status": "match", "detail": detail}
|
||||||
|
if result.exit_code == 1:
|
||||||
|
return {"status": "mismatch", "detail": detail}
|
||||||
|
if result.exit_code == 3:
|
||||||
|
return {"status": "missing", "detail": detail or "user missing"}
|
||||||
|
return {"status": "error", "detail": detail or f"exit_code={result.exit_code}"}
|
||||||
|
|
||||||
def ensure_admin(self, wait: bool = False) -> dict[str, Any]:
|
def ensure_admin(self, wait: bool = False) -> dict[str, Any]:
|
||||||
if not settings.wger_namespace:
|
if not settings.wger_namespace:
|
||||||
raise RuntimeError("wger admin sync not configured")
|
raise RuntimeError("wger admin sync not configured")
|
||||||
@ -415,7 +516,19 @@ class WgerService:
|
|||||||
if isinstance(prepared, UserSyncOutcome):
|
if isinstance(prepared, UserSyncOutcome):
|
||||||
return prepared
|
return prepared
|
||||||
if _should_skip_sync(prepared.password_generated, prepared.updated_at):
|
if _should_skip_sync(prepared.password_generated, prepared.updated_at):
|
||||||
return UserSyncOutcome("skipped")
|
if prepared.rotated_at:
|
||||||
|
return UserSyncOutcome("skipped")
|
||||||
|
check = self.check_password(prepared.username, prepared.password)
|
||||||
|
status = check.get("status") if isinstance(check, dict) else "error"
|
||||||
|
if status == "match":
|
||||||
|
return UserSyncOutcome("skipped")
|
||||||
|
if status == "mismatch":
|
||||||
|
if not _set_wger_rotated_at(prepared.username):
|
||||||
|
return UserSyncOutcome("failed", "failed to set rotated_at")
|
||||||
|
return UserSyncOutcome("synced")
|
||||||
|
detail = check.get("detail") if isinstance(check, dict) else ""
|
||||||
|
detail = detail or "password check failed"
|
||||||
|
return UserSyncOutcome("failed", detail)
|
||||||
|
|
||||||
result = self.sync_user(prepared.username, prepared.mailu_email, prepared.password, wait=True)
|
result = self.sync_user(prepared.username, prepared.mailu_email, prepared.password, wait=True)
|
||||||
result_status = result.get("status") if isinstance(result, dict) else "error"
|
result_status = result.get("status") if isinstance(result, dict) else "error"
|
||||||
|
|||||||
@ -163,6 +163,7 @@ class Settings:
|
|||||||
vaultwarden_admin_rate_limit_backoff_sec: float
|
vaultwarden_admin_rate_limit_backoff_sec: float
|
||||||
vaultwarden_retry_cooldown_sec: float
|
vaultwarden_retry_cooldown_sec: float
|
||||||
vaultwarden_failure_bailout: int
|
vaultwarden_failure_bailout: int
|
||||||
|
vaultwarden_invite_refresh_sec: float
|
||||||
|
|
||||||
smtp_host: str
|
smtp_host: str
|
||||||
smtp_port: int
|
smtp_port: int
|
||||||
@ -420,6 +421,7 @@ class Settings:
|
|||||||
"vaultwarden_admin_rate_limit_backoff_sec": _env_float("VAULTWARDEN_ADMIN_RATE_LIMIT_BACKOFF_SEC", 600.0),
|
"vaultwarden_admin_rate_limit_backoff_sec": _env_float("VAULTWARDEN_ADMIN_RATE_LIMIT_BACKOFF_SEC", 600.0),
|
||||||
"vaultwarden_retry_cooldown_sec": _env_float("VAULTWARDEN_RETRY_COOLDOWN_SEC", 1800.0),
|
"vaultwarden_retry_cooldown_sec": _env_float("VAULTWARDEN_RETRY_COOLDOWN_SEC", 1800.0),
|
||||||
"vaultwarden_failure_bailout": _env_int("VAULTWARDEN_FAILURE_BAILOUT", 2),
|
"vaultwarden_failure_bailout": _env_int("VAULTWARDEN_FAILURE_BAILOUT", 2),
|
||||||
|
"vaultwarden_invite_refresh_sec": _env_float("VAULTWARDEN_INVITE_REFRESH_SEC", 86400.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -429,7 +431,7 @@ class Settings:
|
|||||||
"nextcloud_sync_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_SYNC", "0 5 * * *"),
|
"nextcloud_sync_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_SYNC", "0 5 * * *"),
|
||||||
"nextcloud_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_CRON", "*/5 * * * *"),
|
"nextcloud_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_CRON", "*/5 * * * *"),
|
||||||
"nextcloud_maintenance_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_MAINTENANCE", "30 4 * * *"),
|
"nextcloud_maintenance_cron": _env("ARIADNE_SCHEDULE_NEXTCLOUD_MAINTENANCE", "30 4 * * *"),
|
||||||
"vaultwarden_sync_cron": _env("ARIADNE_SCHEDULE_VAULTWARDEN_SYNC", "*/15 * * * *"),
|
"vaultwarden_sync_cron": _env("ARIADNE_SCHEDULE_VAULTWARDEN_SYNC", "0 * * * *"),
|
||||||
"wger_user_sync_cron": _env("ARIADNE_SCHEDULE_WGER_USER_SYNC", "0 5 * * *"),
|
"wger_user_sync_cron": _env("ARIADNE_SCHEDULE_WGER_USER_SYNC", "0 5 * * *"),
|
||||||
"wger_admin_cron": _env("ARIADNE_SCHEDULE_WGER_ADMIN", "15 3 * * *"),
|
"wger_admin_cron": _env("ARIADNE_SCHEDULE_WGER_ADMIN", "15 3 * * *"),
|
||||||
"firefly_user_sync_cron": _env("ARIADNE_SCHEDULE_FIREFLY_USER_SYNC", "0 6 * * *"),
|
"firefly_user_sync_cron": _env("ARIADNE_SCHEDULE_FIREFLY_USER_SYNC", "0 6 * * *"),
|
||||||
@ -437,9 +439,9 @@ class Settings:
|
|||||||
"pod_cleaner_cron": _env("ARIADNE_SCHEDULE_POD_CLEANER", "0 * * * *"),
|
"pod_cleaner_cron": _env("ARIADNE_SCHEDULE_POD_CLEANER", "0 * * * *"),
|
||||||
"opensearch_prune_cron": _env("ARIADNE_SCHEDULE_OPENSEARCH_PRUNE", "23 3 * * *"),
|
"opensearch_prune_cron": _env("ARIADNE_SCHEDULE_OPENSEARCH_PRUNE", "23 3 * * *"),
|
||||||
"image_sweeper_cron": _env("ARIADNE_SCHEDULE_IMAGE_SWEEPER", "30 4 * * 0"),
|
"image_sweeper_cron": _env("ARIADNE_SCHEDULE_IMAGE_SWEEPER", "30 4 * * 0"),
|
||||||
"vault_k8s_auth_cron": _env("ARIADNE_SCHEDULE_VAULT_K8S_AUTH", "*/15 * * * *"),
|
"vault_k8s_auth_cron": _env("ARIADNE_SCHEDULE_VAULT_K8S_AUTH", "0 * * * *"),
|
||||||
"vault_oidc_cron": _env("ARIADNE_SCHEDULE_VAULT_OIDC", "*/15 * * * *"),
|
"vault_oidc_cron": _env("ARIADNE_SCHEDULE_VAULT_OIDC", "0 * * * *"),
|
||||||
"comms_guest_name_cron": _env("ARIADNE_SCHEDULE_COMMS_GUEST_NAME", "*/1 * * * *"),
|
"comms_guest_name_cron": _env("ARIADNE_SCHEDULE_COMMS_GUEST_NAME", "*/5 * * * *"),
|
||||||
"comms_pin_invite_cron": _env("ARIADNE_SCHEDULE_COMMS_PIN_INVITE", "*/30 * * * *"),
|
"comms_pin_invite_cron": _env("ARIADNE_SCHEDULE_COMMS_PIN_INVITE", "*/30 * * * *"),
|
||||||
"comms_reset_room_cron": _env("ARIADNE_SCHEDULE_COMMS_RESET_ROOM", "0 0 1 1 *"),
|
"comms_reset_room_cron": _env("ARIADNE_SCHEDULE_COMMS_RESET_ROOM", "0 0 1 1 *"),
|
||||||
"comms_seed_room_cron": _env("ARIADNE_SCHEDULE_COMMS_SEED_ROOM", "*/10 * * * *"),
|
"comms_seed_room_cron": _env("ARIADNE_SCHEDULE_COMMS_SEED_ROOM", "*/10 * * * *"),
|
||||||
|
|||||||
@ -186,6 +186,70 @@ def test_wger_sync_users(monkeypatch) -> None:
|
|||||||
assert any(key == "wger_password_updated_at" for _user, key, _value in calls)
|
assert any(key == "wger_password_updated_at" for _user, key, _value in calls)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wger_sync_marks_rotated(monkeypatch) -> None:
|
||||||
|
dummy = types.SimpleNamespace(
|
||||||
|
wger_namespace="health",
|
||||||
|
wger_user_sync_wait_timeout_sec=60.0,
|
||||||
|
wger_pod_label="app=wger",
|
||||||
|
wger_container="wger",
|
||||||
|
wger_admin_username="admin",
|
||||||
|
wger_admin_password="pw",
|
||||||
|
wger_admin_email="admin@bstein.dev",
|
||||||
|
mailu_domain="bstein.dev",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("ariadne.services.wger.settings", dummy)
|
||||||
|
|
||||||
|
calls: list[tuple[str, str, str]] = []
|
||||||
|
|
||||||
|
class DummyAdmin:
|
||||||
|
def ready(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def iter_users(self, page_size=200, brief=False):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"username": "alice",
|
||||||
|
"attributes": {
|
||||||
|
"mailu_email": ["alice@bstein.dev"],
|
||||||
|
"wger_password": ["pw"],
|
||||||
|
"wger_password_updated_at": ["2025-01-01T00:00:00Z"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_user(self, user_id: str):
|
||||||
|
return {
|
||||||
|
"id": user_id,
|
||||||
|
"username": "alice",
|
||||||
|
"attributes": {
|
||||||
|
"mailu_email": ["alice@bstein.dev"],
|
||||||
|
"wger_password": ["pw"],
|
||||||
|
"wger_password_updated_at": ["2025-01-01T00:00:00Z"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_user_attribute(self, username: str, key: str, value: str) -> None:
|
||||||
|
calls.append((username, key, value))
|
||||||
|
|
||||||
|
monkeypatch.setattr("ariadne.services.wger.keycloak_admin", DummyAdmin())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"ariadne.services.wger.mailu.resolve_mailu_email",
|
||||||
|
lambda *_args, **_kwargs: "alice@bstein.dev",
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_check(self, *_args, **_kwargs):
|
||||||
|
return {"status": "mismatch", "detail": "mismatch"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(WgerService, "check_password", fake_check)
|
||||||
|
monkeypatch.setattr(WgerService, "sync_user", lambda *_args, **_kwargs: {"status": "ok"})
|
||||||
|
|
||||||
|
svc = WgerService()
|
||||||
|
result = svc.sync_users()
|
||||||
|
assert result["status"] == "ok"
|
||||||
|
assert any(key == "wger_password_rotated_at" for _user, key, _value in calls)
|
||||||
|
|
||||||
|
|
||||||
def test_firefly_sync_user_exec(monkeypatch) -> None:
|
def test_firefly_sync_user_exec(monkeypatch) -> None:
|
||||||
dummy = types.SimpleNamespace(
|
dummy = types.SimpleNamespace(
|
||||||
firefly_namespace="finance",
|
firefly_namespace="finance",
|
||||||
@ -330,6 +394,70 @@ def test_firefly_sync_users(monkeypatch) -> None:
|
|||||||
assert any(key == "firefly_password_updated_at" for _user, key, _value in calls)
|
assert any(key == "firefly_password_updated_at" for _user, key, _value in calls)
|
||||||
|
|
||||||
|
|
||||||
|
def test_firefly_sync_marks_rotated(monkeypatch) -> None:
|
||||||
|
dummy = types.SimpleNamespace(
|
||||||
|
firefly_namespace="finance",
|
||||||
|
firefly_user_sync_wait_timeout_sec=60.0,
|
||||||
|
firefly_pod_label="app=firefly",
|
||||||
|
firefly_container="firefly",
|
||||||
|
firefly_cron_base_url="http://firefly/cron",
|
||||||
|
firefly_cron_token="token",
|
||||||
|
firefly_cron_timeout_sec=10.0,
|
||||||
|
mailu_domain="bstein.dev",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("ariadne.services.firefly.settings", dummy)
|
||||||
|
|
||||||
|
calls: list[tuple[str, str, str]] = []
|
||||||
|
|
||||||
|
class DummyAdmin:
|
||||||
|
def ready(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def iter_users(self, page_size=200, brief=False):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"username": "alice",
|
||||||
|
"attributes": {
|
||||||
|
"mailu_email": ["alice@bstein.dev"],
|
||||||
|
"firefly_password": ["pw"],
|
||||||
|
"firefly_password_updated_at": ["2025-01-01T00:00:00Z"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_user(self, user_id: str):
|
||||||
|
return {
|
||||||
|
"id": user_id,
|
||||||
|
"username": "alice",
|
||||||
|
"attributes": {
|
||||||
|
"mailu_email": ["alice@bstein.dev"],
|
||||||
|
"firefly_password": ["pw"],
|
||||||
|
"firefly_password_updated_at": ["2025-01-01T00:00:00Z"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_user_attribute(self, username: str, key: str, value: str) -> None:
|
||||||
|
calls.append((username, key, value))
|
||||||
|
|
||||||
|
monkeypatch.setattr("ariadne.services.firefly.keycloak_admin", DummyAdmin())
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"ariadne.services.firefly.mailu.resolve_mailu_email",
|
||||||
|
lambda *_args, **_kwargs: "alice@bstein.dev",
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_check(self, *_args, **_kwargs):
|
||||||
|
return {"status": "mismatch", "detail": "mismatch"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(FireflyService, "check_password", fake_check)
|
||||||
|
monkeypatch.setattr(FireflyService, "sync_user", lambda *_args, **_kwargs: {"status": "ok"})
|
||||||
|
|
||||||
|
svc = FireflyService()
|
||||||
|
result = svc.sync_users()
|
||||||
|
assert result["status"] == "ok"
|
||||||
|
assert any(key == "firefly_password_rotated_at" for _user, key, _value in calls)
|
||||||
|
|
||||||
|
|
||||||
def test_mailu_sync_updates_attrs(monkeypatch) -> None:
|
def test_mailu_sync_updates_attrs(monkeypatch) -> None:
|
||||||
dummy_settings = types.SimpleNamespace(
|
dummy_settings = types.SimpleNamespace(
|
||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
|
|||||||
@ -97,6 +97,7 @@ def test_vaultwarden_sync_respects_retry_cooldown(monkeypatch) -> None:
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=9999,
|
vaultwarden_retry_cooldown_sec=9999,
|
||||||
vaultwarden_failure_bailout=2,
|
vaultwarden_failure_bailout=2,
|
||||||
|
vaultwarden_invite_refresh_sec=9999,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
@ -137,6 +138,7 @@ def test_vaultwarden_sync_bails_after_failures(monkeypatch) -> None:
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=0,
|
vaultwarden_retry_cooldown_sec=0,
|
||||||
vaultwarden_failure_bailout=1,
|
vaultwarden_failure_bailout=1,
|
||||||
|
vaultwarden_invite_refresh_sec=0,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
dummy = DummyAdmin(
|
dummy = DummyAdmin(
|
||||||
@ -162,6 +164,7 @@ def test_vaultwarden_sync_uses_keycloak_email(monkeypatch) -> None:
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=0,
|
vaultwarden_retry_cooldown_sec=0,
|
||||||
vaultwarden_failure_bailout=2,
|
vaultwarden_failure_bailout=2,
|
||||||
|
vaultwarden_invite_refresh_sec=0,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
dummy = DummyAdmin(
|
dummy = DummyAdmin(
|
||||||
@ -265,6 +268,7 @@ def test_vaultwarden_sync_sets_synced_at_for_invited(monkeypatch) -> None:
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=0,
|
vaultwarden_retry_cooldown_sec=0,
|
||||||
vaultwarden_failure_bailout=2,
|
vaultwarden_failure_bailout=2,
|
||||||
|
vaultwarden_invite_refresh_sec=0,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
dummy = DummyAdmin(
|
dummy = DummyAdmin(
|
||||||
@ -284,6 +288,7 @@ def test_vaultwarden_sync_skips_disabled_and_service_accounts(monkeypatch) -> No
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=0,
|
vaultwarden_retry_cooldown_sec=0,
|
||||||
vaultwarden_failure_bailout=2,
|
vaultwarden_failure_bailout=2,
|
||||||
|
vaultwarden_invite_refresh_sec=0,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
dummy = DummyAdmin(
|
dummy = DummyAdmin(
|
||||||
@ -305,6 +310,7 @@ def test_vaultwarden_sync_get_user_failure(monkeypatch) -> None:
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=0,
|
vaultwarden_retry_cooldown_sec=0,
|
||||||
vaultwarden_failure_bailout=2,
|
vaultwarden_failure_bailout=2,
|
||||||
|
vaultwarden_invite_refresh_sec=0,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
|
|
||||||
@ -328,6 +334,7 @@ def test_vaultwarden_sync_invited_attribute_failure(monkeypatch) -> None:
|
|||||||
mailu_domain="bstein.dev",
|
mailu_domain="bstein.dev",
|
||||||
vaultwarden_retry_cooldown_sec=0,
|
vaultwarden_retry_cooldown_sec=0,
|
||||||
vaultwarden_failure_bailout=2,
|
vaultwarden_failure_bailout=2,
|
||||||
|
vaultwarden_invite_refresh_sec=0,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
monkeypatch.setattr(vaultwarden_sync, "settings", dummy_settings)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user