mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-02-08 17:03:11 -05:00
fix: user login and creation with LDAP (#2107)
* Corrected if statement to check if a results was returned by the LDAP search. And decoded the user_attributes from binary data to string * removed trailing spaces * Revert asserts in LDAP unit test back Since an empty tuple is still a result, an user is created and the result should not be false. * Simplified code * Extended the LDAP implementation * fix ldap authentication and user creation * modified docs to include new LDAP environment variables * update tests and linting * add libldap-2.4-2 as runtime dependency for the api --------- Co-authored-by: Erik Landkroon <eriklandkroon@gmail.com>
This commit is contained in:
@@ -54,6 +54,7 @@ def user_from_ldap(db: AllRepositories, username: str, password: str) -> Private
|
||||
|
||||
if settings.LDAP_TLS_INSECURE:
|
||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
|
||||
|
||||
ldap.set_option(ldap.OPT_REFERRALS, 0)
|
||||
ldap.set_option(ldap.OPT_PROTOCOL_VERSION, 3)
|
||||
conn = ldap.initialize(settings.LDAP_SERVER_URL)
|
||||
@@ -61,19 +62,11 @@ def user_from_ldap(db: AllRepositories, username: str, password: str) -> Private
|
||||
if settings.LDAP_TLS_CACERTFILE:
|
||||
conn.set_option(ldap.OPT_X_TLS_CACERTFILE, settings.LDAP_TLS_CACERTFILE)
|
||||
conn.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
|
||||
user = db.users.get_one(username, "email", any_case=True)
|
||||
|
||||
if not settings.LDAP_BIND_TEMPLATE:
|
||||
return False
|
||||
|
||||
if not user:
|
||||
user_bind = settings.LDAP_BIND_TEMPLATE.format(username)
|
||||
user = db.users.get_one(username, "username", any_case=True)
|
||||
else:
|
||||
user_bind = settings.LDAP_BIND_TEMPLATE.format(user.username)
|
||||
|
||||
# Use query user for the search instead of the logged in user
|
||||
# This prevents the need for every user to have query permissions in LDAP
|
||||
try:
|
||||
conn.simple_bind_s(user_bind, password)
|
||||
conn.simple_bind_s(settings.LDAP_QUERY_BIND, settings.LDAP_QUERY_PASSWORD)
|
||||
except (ldap.INVALID_CREDENTIALS, ldap.NO_SUCH_OBJECT):
|
||||
return False
|
||||
|
||||
@@ -83,21 +76,44 @@ def user_from_ldap(db: AllRepositories, username: str, password: str) -> Private
|
||||
user_entry = conn.search_s(
|
||||
settings.LDAP_BASE_DN,
|
||||
ldap.SCOPE_SUBTREE,
|
||||
f"(&(objectClass=user)(|(cn={username})(sAMAccountName={username})(mail={username})))",
|
||||
["name", "mail"],
|
||||
settings.LDAP_USER_FILTER.format(
|
||||
id_attribute=settings.LDAP_ID_ATTRIBUTE, mail_attribute=settings.LDAP_MAIL_ATTRIBUTE, input=username
|
||||
),
|
||||
[settings.LDAP_ID_ATTRIBUTE, settings.LDAP_NAME_ATTRIBUTE, settings.LDAP_MAIL_ATTRIBUTE],
|
||||
)
|
||||
if user_entry is not None and len(user_entry[0]) != 0 and user_entry[0][0] is not None:
|
||||
user_dn, user_attr = user_entry[0]
|
||||
else:
|
||||
|
||||
if not user_entry:
|
||||
conn.unbind_s()
|
||||
return False
|
||||
|
||||
user_dn, user_attr = user_entry[0]
|
||||
|
||||
# Check the credentials of the user
|
||||
try:
|
||||
conn.simple_bind_s(user_dn, password)
|
||||
except (ldap.INVALID_CREDENTIALS, ldap.NO_SUCH_OBJECT):
|
||||
return False
|
||||
|
||||
# Check for existing user
|
||||
user = db.users.get_one(username, "email", any_case=True)
|
||||
if not user:
|
||||
user = db.users.get_one(username, "username", any_case=True)
|
||||
|
||||
if user is None:
|
||||
try:
|
||||
user_id = user_attr[settings.LDAP_ID_ATTRIBUTE][0].decode("utf-8")
|
||||
full_name = user_attr[settings.LDAP_NAME_ATTRIBUTE][0].decode("utf-8")
|
||||
email = user_attr[settings.LDAP_MAIL_ATTRIBUTE][0].decode("utf-8")
|
||||
except KeyError:
|
||||
conn.unbind_s()
|
||||
return False
|
||||
|
||||
user = db.users.create(
|
||||
{
|
||||
"username": username,
|
||||
"username": user_id,
|
||||
"password": "LDAP",
|
||||
"full_name": user_attr["name"][0],
|
||||
"email": user_attr["mail"][0],
|
||||
"full_name": full_name,
|
||||
"email": email,
|
||||
"admin": False,
|
||||
},
|
||||
)
|
||||
@@ -105,6 +121,8 @@ def user_from_ldap(db: AllRepositories, username: str, password: str) -> Private
|
||||
if settings.LDAP_ADMIN_FILTER:
|
||||
user.admin = len(conn.search_s(user_dn, ldap.SCOPE_BASE, settings.LDAP_ADMIN_FILTER, [])) > 0
|
||||
db.users.update(user.id, user)
|
||||
|
||||
conn.unbind_s()
|
||||
return user
|
||||
|
||||
|
||||
|
||||
@@ -117,18 +117,27 @@ class AppSettings(BaseSettings):
|
||||
LDAP_SERVER_URL: NoneStr = None
|
||||
LDAP_TLS_INSECURE: bool = False
|
||||
LDAP_TLS_CACERTFILE: NoneStr = None
|
||||
LDAP_BIND_TEMPLATE: NoneStr = None
|
||||
LDAP_BASE_DN: NoneStr = None
|
||||
LDAP_QUERY_BIND: NoneStr = None
|
||||
LDAP_QUERY_PASSWORD: NoneStr = None
|
||||
LDAP_USER_FILTER: NoneStr = None
|
||||
LDAP_ADMIN_FILTER: NoneStr = None
|
||||
LDAP_ID_ATTRIBUTE: str = "uid"
|
||||
LDAP_MAIL_ATTRIBUTE: str = "mail"
|
||||
LDAP_NAME_ATTRIBUTE: str = "name"
|
||||
|
||||
@property
|
||||
def LDAP_ENABLED(self) -> bool:
|
||||
"""Validates LDAP settings are all set"""
|
||||
required = {
|
||||
self.LDAP_SERVER_URL,
|
||||
self.LDAP_BIND_TEMPLATE,
|
||||
self.LDAP_BASE_DN,
|
||||
self.LDAP_ADMIN_FILTER,
|
||||
self.LDAP_USER_FILTER,
|
||||
self.LDAP_QUERY_BIND,
|
||||
self.LDAP_QUERY_PASSWORD,
|
||||
self.LDAP_ID_ATTRIBUTE,
|
||||
self.LDAP_MAIL_ATTRIBUTE,
|
||||
self.LDAP_NAME_ATTRIBUTE,
|
||||
}
|
||||
not_none = None not in required
|
||||
return self.LDAP_AUTH_ENABLED and not_none
|
||||
|
||||
Reference in New Issue
Block a user