"""A fake directory, for tests only — D13 / T10.7. `server/ldap_auth.py` normally opens an LDAPS connection to a domain controller. Tests cannot: CI has no domain, and the browser checks launch the app as a SUBPROCESS (`start_server` in tests/browser_check.py), so a monkeypatch in the test process would never reach the code doing the authenticating. The seam has to be configurable from the ENVIRONMENT, which is what this module is. Set `WP_LDAP_FAKE_DIRECTORY` to a JSON object and `ldap_auth.verify()` answers from it instead of touching the network: {"root": {"password": "…", "mail": "root@example.test", "full_name": "Root", "groups": ["WP-Suite-Users"]}} `groups` is a flat list of names the account is "in". Nested groups do not exist here — the real `member_of` resolves a DN and uses AD's LDAP_MATCHING_RULE_IN_CHAIN, and faking that faithfully would mean reimplementing AD. A test that cares about nesting has to run against a real directory; this one is honest about being a string comparison. THE PRODUCTION GUARD IS THE POINT OF THIS FILE. An environment variable that makes any password work is exactly the kind of thing that escapes into production, and D13 removed every other way in — there is no local password to fall back on and no break-glass, so a fake directory silently active in production would be a total authentication bypass with nothing behind it. So `is_active()` refuses whenever a real database is configured, using the same test `auth._load_secret` uses to refuse an ephemeral signing key: a non-SQLite `DATABASE_URL` means production, full stop. `ldap_auth.describe()` also shouts when the fake is live, so the startup line can never be mistaken for a real one. """ import json import logging import os from typing import Optional log = logging.getLogger("wpsuite.ldap.fake") ENV_VAR = "WP_LDAP_FAKE_DIRECTORY" def _raw() -> str: return os.getenv(ENV_VAR, "").strip() def is_active() -> bool: """Whether the fake should answer. False in anything resembling production.""" if not _raw(): return False # Imported lazily: server.db reads DATABASE_URL at import, and this module is # imported from ldap_auth, which must stay importable on its own. from .db import DATABASE_URL if not str(DATABASE_URL).startswith("sqlite"): log.error( "%s is set but a non-SQLite DATABASE_URL is configured. REFUSING to use " "the fake directory — this looks like production, and D13 leaves no " "other way in, so honouring it would be an authentication bypass. " "Unset %s.", ENV_VAR, ENV_VAR) return False return True def directory() -> dict: try: data = json.loads(_raw()) if not isinstance(data, dict): raise ValueError("top level must be an object") return data except Exception as exc: # noqa: BLE001 — a malformed fake must not look like a bad password log.error("%s is not valid JSON (%s); the fake directory is empty", ENV_VAR, exc) return {} def lookup(username: str, password: str, required_group: Optional[str]) -> tuple: """Return (ok, reason, attrs). Mirrors what ldap_auth.verify() needs. Deliberately does NOT re-check for an empty password: `verify()` guards that before it ever gets here, and duplicating the check in the fake would let the real guard rot without any test noticing. """ people = directory() who = people.get(username) or people.get(username.lower()) if not isinstance(who, dict) or password != who.get("password"): return (False, "bad_credentials", {}) if required_group: groups = who.get("groups") or [] if required_group not in groups: return (False, "not_in_group", {}) return (True, "ok", { "sam": who.get("sam") or username, "mail": who.get("mail", ""), "full_name": who.get("full_name", ""), "upn": who.get("upn", ""), })