ldap: initial implementation
Introduce an ldap component that can be used authenticate Moonraker users over ldap connections. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
5081321a32
commit
f52df8c7ed
|
@ -0,0 +1,116 @@
|
||||||
|
# LDAP authentication for Moonraker
|
||||||
|
#
|
||||||
|
# Copyright (C) 2022 Eric Callahan <arksine.code@gmail.com>
|
||||||
|
# Copyright (C) 2022 Luca Schöneberg <luca-schoeneberg@outlook.com>
|
||||||
|
#
|
||||||
|
# This file may be distributed under the terms of the GNU GPLv3 license
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import ldap3
|
||||||
|
from ldap3.core.exceptions import LDAPExceptionError
|
||||||
|
|
||||||
|
# Annotation imports
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Optional
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from confighelper import ConfigHelper
|
||||||
|
from ldap3.abstract.entry import Entry
|
||||||
|
|
||||||
|
class MoonrakerLDAP:
|
||||||
|
def __init__(self, config: ConfigHelper) -> None:
|
||||||
|
self.server = config.get_server()
|
||||||
|
self.ldap_host = config.get('ldap_host')
|
||||||
|
self.ldap_port = config.getint("ldap_port", None)
|
||||||
|
self.ldap_secure = config.getboolean("ldap_secure", False)
|
||||||
|
base_dn_template = config.gettemplate('base_dn')
|
||||||
|
self.base_dn = base_dn_template.render()
|
||||||
|
self.group_dn: Optional[str] = None
|
||||||
|
group_dn_template = config.gettemplate("group_dn", None)
|
||||||
|
if group_dn_template is not None:
|
||||||
|
self.group_dn = group_dn_template.render()
|
||||||
|
self.active_directory = config.getboolean('is_active_directory', False)
|
||||||
|
self.bind_dn: Optional[str] = None
|
||||||
|
self.bind_password: Optional[str] = None
|
||||||
|
bind_dn_template = config.gettemplate('bind_dn', None)
|
||||||
|
bind_pass_template = config.gettemplate('bind_password', None)
|
||||||
|
if bind_dn_template is not None:
|
||||||
|
self.bind_dn = bind_dn_template.render()
|
||||||
|
if bind_pass_template is None:
|
||||||
|
raise config.error(
|
||||||
|
"Section [ldap]: Option 'bind_password' is "
|
||||||
|
"required when 'bind_dn' is provided"
|
||||||
|
)
|
||||||
|
self.bind_password = bind_pass_template.render()
|
||||||
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def authenticate_ldap_user(self, username, password) -> None:
|
||||||
|
eventloop = self.server.get_event_loop()
|
||||||
|
async with self.lock:
|
||||||
|
await eventloop.run_in_thread(
|
||||||
|
self._perform_ldap_auth, username, password
|
||||||
|
)
|
||||||
|
|
||||||
|
def _perform_ldap_auth(self, username, password) -> None:
|
||||||
|
server = ldap3.Server(
|
||||||
|
self.ldap_host, self.ldap_port, use_ssl=self.ldap_secure,
|
||||||
|
connect_timeout=10.
|
||||||
|
)
|
||||||
|
conn_args = {
|
||||||
|
"user": self.bind_dn,
|
||||||
|
"password": self.bind_password,
|
||||||
|
"auto_bind": ldap3.AUTO_BIND_NO_TLS,
|
||||||
|
}
|
||||||
|
attr_name = "sAMAccountName" if self.active_directory else "uid"
|
||||||
|
ldfilt = f"(&(objectClass=Person)({attr_name}={username}))"
|
||||||
|
try:
|
||||||
|
with ldap3.Connection(server, **conn_args) as conn:
|
||||||
|
ret = conn.search(
|
||||||
|
self.base_dn, ldfilt, attributes=["memberOf"]
|
||||||
|
)
|
||||||
|
if not ret:
|
||||||
|
raise self.server.error(
|
||||||
|
f"LDAP User '{username}' Not Found", 401
|
||||||
|
)
|
||||||
|
user: Entry = conn.entries[0]
|
||||||
|
rebind_success = conn.rebind(user.entry_dn, password)
|
||||||
|
if not rebind_success:
|
||||||
|
# Server may not allow rebinding, attempt to start
|
||||||
|
# a new connection to validate credentials
|
||||||
|
logging.debug(
|
||||||
|
"LDAP Rebind failed, attempting to validate credentials "
|
||||||
|
"with new connection."
|
||||||
|
)
|
||||||
|
conn_args["user"] = user.entry_dn
|
||||||
|
conn_args["password"] = password
|
||||||
|
with ldap3.Connection(server, **conn_args) as conn:
|
||||||
|
if self._validate_group(username, user):
|
||||||
|
return
|
||||||
|
elif self._validate_group(username, user):
|
||||||
|
return
|
||||||
|
except LDAPExceptionError:
|
||||||
|
err_msg = "LDAP authentication failed"
|
||||||
|
else:
|
||||||
|
err_msg = "Invalid LDAP Username or Password"
|
||||||
|
raise self.server.error(err_msg, 401)
|
||||||
|
|
||||||
|
def _validate_group(self, username: str, user: Entry) -> bool:
|
||||||
|
if self.group_dn is None:
|
||||||
|
logging.debug(f"LDAP User {username} login successful")
|
||||||
|
return True
|
||||||
|
for group in user.memberOf.value:
|
||||||
|
if group == self.group_dn:
|
||||||
|
logging.debug(
|
||||||
|
f"LDAP User {username} group match success, "
|
||||||
|
"login successful"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_component(config: ConfigHelper) -> MoonrakerLDAP:
|
||||||
|
return MoonrakerLDAP(config)
|
Loading…
Reference in New Issue