100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""LDAP authentication tests.
|
|
|
|
The blocking directory bind (``_authenticate_sync``) is replaced with a fake so
|
|
no real LDAP server is needed; everything above it — the enabled gate, the
|
|
threadpool hop, JIT provisioning, and local fallback — runs for real.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from app.core.config import settings
|
|
from app.crud import user as user_crud
|
|
from app.services import ldap as ldap_service
|
|
|
|
DIRECTORY = {
|
|
# login name -> (password, email, full_name)
|
|
"jdoe": ("ldap-pass", "jdoe@corp.example", "Jane Doe"),
|
|
}
|
|
|
|
|
|
def _fake_sync(username: str, password: str):
|
|
record = DIRECTORY.get(username)
|
|
if record is None or record[0] != password:
|
|
return None
|
|
_, email, full_name = record
|
|
return ldap_service.LdapUser(email=email.lower(), full_name=full_name)
|
|
|
|
|
|
@pytest.fixture
|
|
def ldap_enabled(monkeypatch):
|
|
monkeypatch.setattr(settings, "ldap_enabled", True)
|
|
monkeypatch.setattr(settings, "ldap_default_roles", ["user"])
|
|
monkeypatch.setattr(ldap_service, "_authenticate_sync", _fake_sync)
|
|
|
|
|
|
async def test_ldap_login_provisions_local_user(
|
|
client, seeded, db_sessionmaker, ldap_enabled
|
|
):
|
|
resp = await client.post(
|
|
"/api/auth/login",
|
|
data={"username": "jdoe", "password": "ldap-pass"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
access = resp.json()["access_token"]
|
|
|
|
# A local row was created from the directory entry.
|
|
async with db_sessionmaker() as db:
|
|
user = await user_crud.get_by_email(db, "jdoe@corp.example")
|
|
assert user is not None
|
|
assert user.auth_provider == "ldap"
|
|
assert user.full_name == "Jane Doe"
|
|
assert "user" in {r.name for r in user.roles} # default role applied
|
|
|
|
# The issued token authenticates against the API.
|
|
me = await client.get(
|
|
"/api/users/me", headers={"Authorization": f"Bearer {access}"}
|
|
)
|
|
assert me.status_code == 200
|
|
assert me.json()["email"] == "jdoe@corp.example"
|
|
assert me.json()["auth_provider"] == "ldap"
|
|
|
|
|
|
async def test_ldap_wrong_password_rejected(client, seeded, ldap_enabled):
|
|
resp = await client.post(
|
|
"/api/auth/login",
|
|
data={"username": "jdoe", "password": "nope"},
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
async def test_ldap_user_cannot_use_local_password(
|
|
client, seeded, db_sessionmaker, ldap_enabled
|
|
):
|
|
# Provision via LDAP first.
|
|
await client.post(
|
|
"/api/auth/login", data={"username": "jdoe", "password": "ldap-pass"}
|
|
)
|
|
# The local password slot is unusable, so DB auth must fail for this user
|
|
# even though the account now exists.
|
|
async with db_sessionmaker() as db:
|
|
assert (
|
|
await user_crud.authenticate(db, "jdoe@corp.example", "ldap-pass") is None
|
|
)
|
|
|
|
|
|
async def test_local_user_still_logs_in_when_ldap_enabled(
|
|
client, seeded, make_user, ldap_enabled
|
|
):
|
|
# A local account that does NOT exist in the directory falls back to DB auth.
|
|
await make_user("local@example.com", password="secret123")
|
|
resp = await client.post(
|
|
"/api/auth/login",
|
|
data={"username": "local@example.com", "password": "secret123"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
async def test_empty_password_is_rejected(ldap_enabled):
|
|
# Guards against an anonymous bind masquerading as a successful login.
|
|
assert await ldap_service.authenticate_ldap("jdoe", "") is None
|