feat: add Windows DNS integration and LDAP/AD authentication
Windows DNS (WinRM): - New dns_service.py: create/delete A-records via PowerShell over WinRM (NTLM) - Idempotent create (removes existing record first), graceful delete - DNS failures are non-fatal — deployment continues, error logged - test-dns endpoint: GET /api/settings/test-dns - Integrated into deploy_customer() and undeploy_customer() LDAP / Active Directory auth: - New ldap_service.py: service-account bind + user search + user bind (ldap3) - Optional AD group restriction via ldap_group_dn - Login flow: LDAP first → local fallback (prevents admin lockout) - LDAP users auto-created with auth_provider="ldap" and role="viewer" - test-ldap endpoint: GET /api/settings/test-ldap - reset-password/reset-mfa guards extended to block LDAP users All credentials (dns_password, ldap_bind_password) encrypted with Fernet. New DB columns added via backwards-compatible migrations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,23 @@ class AppConfig:
|
||||
dashboard_base_port: int
|
||||
ssl_mode: str
|
||||
wildcard_cert_id: int | None
|
||||
# Windows DNS
|
||||
dns_enabled: bool = False
|
||||
dns_server: str = ""
|
||||
dns_username: str = ""
|
||||
dns_password: str = "" # decrypted
|
||||
dns_zone: str = ""
|
||||
dns_record_ip: str = ""
|
||||
# LDAP
|
||||
ldap_enabled: bool = False
|
||||
ldap_server: str = ""
|
||||
ldap_port: int = 389
|
||||
ldap_use_ssl: bool = False
|
||||
ldap_bind_dn: str = ""
|
||||
ldap_bind_password: str = "" # decrypted
|
||||
ldap_base_dn: str = ""
|
||||
ldap_user_filter: str = "(sAMAccountName={username})"
|
||||
ldap_group_dn: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -92,6 +109,14 @@ def get_system_config(db: Session) -> Optional[AppConfig]:
|
||||
npm_password = decrypt_value(row.npm_api_password_encrypted)
|
||||
except Exception:
|
||||
npm_password = ""
|
||||
try:
|
||||
dns_password = decrypt_value(row.dns_password_encrypted) if row.dns_password_encrypted else ""
|
||||
except Exception:
|
||||
dns_password = ""
|
||||
try:
|
||||
ldap_bind_password = decrypt_value(row.ldap_bind_password_encrypted) if row.ldap_bind_password_encrypted else ""
|
||||
except Exception:
|
||||
ldap_bind_password = ""
|
||||
|
||||
return AppConfig(
|
||||
base_domain=row.base_domain,
|
||||
@@ -109,4 +134,19 @@ def get_system_config(db: Session) -> Optional[AppConfig]:
|
||||
dashboard_base_port=getattr(row, "dashboard_base_port", 9000) or 9000,
|
||||
ssl_mode=getattr(row, "ssl_mode", "letsencrypt") or "letsencrypt",
|
||||
wildcard_cert_id=getattr(row, "wildcard_cert_id", None),
|
||||
dns_enabled=bool(getattr(row, "dns_enabled", False)),
|
||||
dns_server=getattr(row, "dns_server", "") or "",
|
||||
dns_username=getattr(row, "dns_username", "") or "",
|
||||
dns_password=dns_password,
|
||||
dns_zone=getattr(row, "dns_zone", "") or "",
|
||||
dns_record_ip=getattr(row, "dns_record_ip", "") or "",
|
||||
ldap_enabled=bool(getattr(row, "ldap_enabled", False)),
|
||||
ldap_server=getattr(row, "ldap_server", "") or "",
|
||||
ldap_port=getattr(row, "ldap_port", 389) or 389,
|
||||
ldap_use_ssl=bool(getattr(row, "ldap_use_ssl", False)),
|
||||
ldap_bind_dn=getattr(row, "ldap_bind_dn", "") or "",
|
||||
ldap_bind_password=ldap_bind_password,
|
||||
ldap_base_dn=getattr(row, "ldap_base_dn", "") or "",
|
||||
ldap_user_filter=getattr(row, "ldap_user_filter", "(sAMAccountName={username})") or "(sAMAccountName={username})",
|
||||
ldap_group_dn=getattr(row, "ldap_group_dn", "") or "",
|
||||
)
|
||||
|
||||
@@ -137,6 +137,23 @@ class SystemConfigUpdate(BaseModel):
|
||||
None, max_length=255,
|
||||
description="Azure AD group object ID. If set, only members of this group can log in."
|
||||
)
|
||||
# Windows DNS
|
||||
dns_enabled: Optional[bool] = None
|
||||
dns_server: Optional[str] = Field(None, max_length=255)
|
||||
dns_username: Optional[str] = Field(None, max_length=255)
|
||||
dns_password: Optional[str] = None # plaintext, encrypted before storage
|
||||
dns_zone: Optional[str] = Field(None, max_length=255)
|
||||
dns_record_ip: Optional[str] = Field(None, max_length=45)
|
||||
# LDAP
|
||||
ldap_enabled: Optional[bool] = None
|
||||
ldap_server: Optional[str] = Field(None, max_length=255)
|
||||
ldap_port: Optional[int] = Field(None, ge=1, le=65535)
|
||||
ldap_use_ssl: Optional[bool] = None
|
||||
ldap_bind_dn: Optional[str] = Field(None, max_length=500)
|
||||
ldap_bind_password: Optional[str] = None # plaintext, encrypted before storage
|
||||
ldap_base_dn: Optional[str] = Field(None, max_length=500)
|
||||
ldap_user_filter: Optional[str] = Field(None, max_length=255)
|
||||
ldap_group_dn: Optional[str] = Field(None, max_length=500)
|
||||
|
||||
@field_validator("ssl_mode")
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user