mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-18 20:29:44 -05:00
5d4e693a2b
Introduce a guided LDAP configuration wizard mirroring the OIDC flow: five-step UI with server/TLS, bind, directory layout, groups and AUTH_METHOD, then optional connection test and .env / Docker Compose generation for copy-paste deployment. - Refactor LDAPService.test_connection to accept an optional config mapping so the wizard can test draft values without merging live env secrets; keep POST /admin/ldap/test on current_app.config. - Add GET /admin/ldap/setup-wizard plus POST endpoints for test, validate, and generate-config (manage_settings, rate limited). - Surface an LDAP card with status badge and wizard link on the integrations list for admins and manage_settings users. - Add tests for validate, generate, and wizard test delegation.
181 lines
6.2 KiB
Python
181 lines
6.2 KiB
Python
"""Tests for LDAP setup wizard admin routes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
def test_ldap_setup_wizard_requires_login(client):
|
|
resp = client.get("/admin/ldap/setup-wizard", follow_redirects=False)
|
|
assert resp.status_code in (302, 401)
|
|
|
|
|
|
def test_ldap_setup_wizard_get_as_admin(admin_authenticated_client):
|
|
resp = admin_authenticated_client.get("/admin/ldap/setup-wizard")
|
|
assert resp.status_code == 200
|
|
assert b"LDAP Setup Wizard" in resp.data
|
|
|
|
|
|
def test_ldap_wizard_validate_missing_host(admin_authenticated_client):
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/validate-config",
|
|
json={"AUTH_METHOD": "ldap"},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.get_json()
|
|
assert body["valid"] is False
|
|
assert any(e.get("field") == "LDAP_HOST" for e in body.get("errors", []))
|
|
|
|
|
|
def test_ldap_wizard_validate_invalid_auth_method(admin_authenticated_client):
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/validate-config",
|
|
json={
|
|
"LDAP_HOST": "ldap.example.com",
|
|
"LDAP_BIND_DN": "cn=reader,dc=example,dc=com",
|
|
"LDAP_BIND_PASSWORD": "x",
|
|
"LDAP_BASE_DN": "dc=example,dc=com",
|
|
"LDAP_USER_LOGIN_ATTR": "uid",
|
|
"AUTH_METHOD": "oidc",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.get_json()
|
|
assert body["valid"] is False
|
|
|
|
|
|
def test_ldap_wizard_validate_ok(admin_authenticated_client):
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/validate-config",
|
|
json={
|
|
"LDAP_HOST": "ldap.example.com",
|
|
"LDAP_BIND_DN": "cn=reader,dc=example,dc=com",
|
|
"LDAP_BIND_PASSWORD": "secret",
|
|
"LDAP_BASE_DN": "dc=example,dc=com",
|
|
"LDAP_USER_LOGIN_ATTR": "uid",
|
|
"AUTH_METHOD": "all",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.get_json()
|
|
assert body["valid"] is True
|
|
assert body["errors"] == []
|
|
|
|
|
|
def test_ldap_wizard_generate_rejects_non_ldap_auth(admin_authenticated_client):
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/generate-config",
|
|
json={
|
|
"AUTH_METHOD": "oidc",
|
|
"LDAP_HOST": "ldap.example.com",
|
|
"LDAP_BIND_DN": "cn=x",
|
|
"LDAP_BIND_PASSWORD": "p",
|
|
"LDAP_BASE_DN": "dc=x",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_ldap_wizard_generate_success(admin_authenticated_client):
|
|
payload = {
|
|
"AUTH_METHOD": "ldap",
|
|
"LDAP_HOST": "ldap.example.com",
|
|
"LDAP_PORT": 636,
|
|
"LDAP_USE_SSL": True,
|
|
"LDAP_USE_TLS": False,
|
|
"LDAP_BIND_DN": "cn=reader,dc=example,dc=com",
|
|
"LDAP_BIND_PASSWORD": "s3cret",
|
|
"LDAP_BASE_DN": "dc=example,dc=com",
|
|
"LDAP_USER_DN": "ou=users",
|
|
"LDAP_USER_OBJECT_CLASS": "inetOrgPerson",
|
|
"LDAP_USER_LOGIN_ATTR": "uid",
|
|
"LDAP_USER_EMAIL_ATTR": "mail",
|
|
"LDAP_USER_FNAME_ATTR": "givenName",
|
|
"LDAP_USER_LNAME_ATTR": "sn",
|
|
"LDAP_GROUP_DN": "ou=groups",
|
|
"LDAP_GROUP_OBJECT_CLASS": "groupOfNames",
|
|
"LDAP_ADMIN_GROUP": "admins",
|
|
"LDAP_REQUIRED_GROUP": "",
|
|
"LDAP_TLS_CA_CERT_FILE": "",
|
|
"LDAP_TIMEOUT": 15,
|
|
}
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/generate-config",
|
|
json=payload,
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.get_json()
|
|
assert body["success"] is True
|
|
assert "AUTH_METHOD=ldap" in body["env_content"]
|
|
assert "LDAP_HOST=ldap.example.com" in body["env_content"]
|
|
assert "LDAP_USE_SSL=true" in body["env_content"]
|
|
assert "LDAP_USE_TLS=false" in body["env_content"]
|
|
assert "LDAP_BIND_PASSWORD=s3cret" in body["env_content"]
|
|
assert "LDAP_PORT=636" in body["env_content"]
|
|
assert "- LDAP_HOST=" in body["docker_compose_content"]
|
|
|
|
|
|
def test_ldap_wizard_generate_requires_host(admin_authenticated_client):
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/generate-config",
|
|
json={
|
|
"AUTH_METHOD": "ldap",
|
|
"LDAP_HOST": "",
|
|
"LDAP_BIND_DN": "cn=x",
|
|
"LDAP_BIND_PASSWORD": "p",
|
|
"LDAP_BASE_DN": "dc=x",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_ldap_wizard_test_connection_uses_service(admin_authenticated_client):
|
|
fake = {"success": True, "message": "Connected successfully", "user_count": 2}
|
|
with patch("app.services.ldap_service.LDAPService.test_connection", return_value=fake) as m:
|
|
resp = admin_authenticated_client.post(
|
|
"/admin/ldap/setup-wizard/test-connection",
|
|
json={
|
|
"LDAP_HOST": "ldap.internal",
|
|
"LDAP_PORT": 389,
|
|
"LDAP_BIND_DN": "cn=x,dc=y",
|
|
"LDAP_BIND_PASSWORD": "pw",
|
|
"LDAP_BASE_DN": "dc=y",
|
|
},
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.get_json() == fake
|
|
assert m.called
|
|
cfg_arg = m.call_args[0][0]
|
|
assert cfg_arg["LDAP_HOST"] == "ldap.internal"
|
|
assert cfg_arg["LDAP_BIND_PASSWORD"] == "pw"
|
|
|
|
|
|
def test_ldap_service_test_connection_accepts_cfg(app):
|
|
"""Explicit cfg dict is used instead of live app config when provided."""
|
|
from app.services.ldap_service import LDAPService
|
|
|
|
cfg = {
|
|
"LDAP_HOST": "draft.example",
|
|
"LDAP_PORT": 389,
|
|
"LDAP_USE_SSL": False,
|
|
"LDAP_USE_TLS": False,
|
|
"LDAP_BIND_DN": "",
|
|
"LDAP_BIND_PASSWORD": "",
|
|
"LDAP_BASE_DN": "dc=x",
|
|
"LDAP_USER_DN": "",
|
|
"LDAP_USER_OBJECT_CLASS": "inetOrgPerson",
|
|
"LDAP_TIMEOUT": 1,
|
|
}
|
|
with app.app_context():
|
|
with patch("app.services.ldap_service._service_connection", return_value=None):
|
|
result = LDAPService.test_connection(cfg)
|
|
assert result["success"] is False
|
|
assert "Could not create LDAP connection" in result["message"]
|