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:
2026-02-21 21:06:51 +01:00
parent bc9aa6624f
commit 7793ca3666
11 changed files with 623 additions and 15 deletions

View File

@@ -172,6 +172,28 @@ class SystemConfig(Base):
String(255), nullable=True,
comment="If set, only Azure AD users in this group (object ID) are allowed to log in."
)
# Windows DNS integration
dns_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
dns_server: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
dns_username: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
dns_password_encrypted: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
dns_zone: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
dns_record_ip: Mapped[Optional[str]] = mapped_column(String(45), nullable=True)
# LDAP / Active Directory authentication
ldap_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
ldap_server: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
ldap_port: Mapped[int] = mapped_column(Integer, default=389)
ldap_use_ssl: Mapped[bool] = mapped_column(Boolean, default=False)
ldap_bind_dn: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
ldap_bind_password_encrypted: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
ldap_base_dn: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
ldap_user_filter: Mapped[Optional[str]] = mapped_column(
String(255), default="(sAMAccountName={username})"
)
ldap_group_dn: Mapped[Optional[str]] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow
@@ -208,6 +230,21 @@ class SystemConfig(Base):
"azure_client_id": self.azure_client_id or "",
"azure_client_secret_set": bool(self.azure_client_secret_encrypted),
"azure_allowed_group_id": self.azure_allowed_group_id or "",
"dns_enabled": bool(self.dns_enabled),
"dns_server": self.dns_server or "",
"dns_username": self.dns_username or "",
"dns_password_set": bool(self.dns_password_encrypted),
"dns_zone": self.dns_zone or "",
"dns_record_ip": self.dns_record_ip or "",
"ldap_enabled": bool(self.ldap_enabled),
"ldap_server": self.ldap_server or "",
"ldap_port": self.ldap_port or 389,
"ldap_use_ssl": bool(self.ldap_use_ssl),
"ldap_bind_dn": self.ldap_bind_dn or "",
"ldap_bind_password_set": bool(self.ldap_bind_password_encrypted),
"ldap_base_dn": self.ldap_base_dn or "",
"ldap_user_filter": self.ldap_user_filter or "(sAMAccountName={username})",
"ldap_group_dn": self.ldap_group_dn or "",
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}